@webex/contact-center 3.11.0 → 3.12.0-mobius-socket.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/cc.js +121 -28
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +5 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/index.js +7 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/metrics/behavioral-events.js +13 -0
  8. package/dist/metrics/behavioral-events.js.map +1 -1
  9. package/dist/metrics/constants.js +9 -1
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/ApiAiAssistant.js +173 -0
  12. package/dist/services/ApiAiAssistant.js.map +1 -0
  13. package/dist/services/agent/types.js.map +1 -1
  14. package/dist/services/config/Util.js +6 -2
  15. package/dist/services/config/Util.js.map +1 -1
  16. package/dist/services/config/constants.js +12 -0
  17. package/dist/services/config/constants.js.map +1 -1
  18. package/dist/services/config/index.js +41 -2
  19. package/dist/services/config/index.js.map +1 -1
  20. package/dist/services/config/types.js +19 -1
  21. package/dist/services/config/types.js.map +1 -1
  22. package/dist/services/constants.js +27 -1
  23. package/dist/services/constants.js.map +1 -1
  24. package/dist/services/core/Err.js.map +1 -1
  25. package/dist/services/core/Utils.js +28 -6
  26. package/dist/services/core/Utils.js.map +1 -1
  27. package/dist/services/core/aqm-reqs.js +92 -17
  28. package/dist/services/core/aqm-reqs.js.map +1 -1
  29. package/dist/services/core/websocket/WebSocketManager.js +20 -5
  30. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  31. package/dist/services/core/websocket/connection-service.js +3 -1
  32. package/dist/services/core/websocket/connection-service.js.map +1 -1
  33. package/dist/services/index.js +6 -0
  34. package/dist/services/index.js.map +1 -1
  35. package/dist/services/task/TaskManager.js +117 -24
  36. package/dist/services/task/TaskManager.js.map +1 -1
  37. package/dist/services/task/TaskUtils.js +16 -3
  38. package/dist/services/task/TaskUtils.js.map +1 -1
  39. package/dist/services/task/constants.js +15 -1
  40. package/dist/services/task/constants.js.map +1 -1
  41. package/dist/services/task/dialer.js +51 -0
  42. package/dist/services/task/dialer.js.map +1 -1
  43. package/dist/services/task/types.js +15 -0
  44. package/dist/services/task/types.js.map +1 -1
  45. package/dist/types/cc.d.ts +801 -0
  46. package/dist/types/config.d.ts +66 -0
  47. package/dist/types/constants.d.ts +50 -0
  48. package/dist/types/index.d.ts +184 -0
  49. package/dist/types/logger-proxy.d.ts +71 -0
  50. package/dist/types/metrics/MetricsManager.d.ts +223 -0
  51. package/dist/types/metrics/behavioral-events.d.ts +29 -0
  52. package/dist/types/metrics/constants.d.ts +161 -0
  53. package/dist/types/services/AddressBook.d.ts +74 -0
  54. package/dist/types/services/ApiAiAssistant.d.ts +31 -0
  55. package/dist/types/services/EntryPoint.d.ts +67 -0
  56. package/dist/types/services/Queue.d.ts +76 -0
  57. package/dist/types/services/WebCallingService.d.ts +1 -0
  58. package/dist/types/services/agent/index.d.ts +46 -0
  59. package/dist/types/services/agent/types.d.ts +413 -0
  60. package/dist/types/services/config/Util.d.ts +20 -0
  61. package/dist/types/services/config/constants.d.ts +249 -0
  62. package/dist/types/services/config/index.d.ts +177 -0
  63. package/dist/types/services/config/types.d.ts +1207 -0
  64. package/dist/types/services/constants.d.ts +110 -0
  65. package/dist/types/services/core/Err.d.ts +121 -0
  66. package/dist/types/services/core/GlobalTypes.d.ts +58 -0
  67. package/dist/types/services/core/Utils.d.ts +101 -0
  68. package/dist/types/services/core/WebexRequest.d.ts +22 -0
  69. package/dist/types/services/core/aqm-reqs.d.ts +65 -0
  70. package/dist/types/services/core/constants.d.ts +99 -0
  71. package/dist/types/services/core/types.d.ts +47 -0
  72. package/dist/types/services/core/websocket/WebSocketManager.d.ts +35 -0
  73. package/dist/types/services/core/websocket/connection-service.d.ts +27 -0
  74. package/dist/types/services/core/websocket/keepalive.worker.d.ts +2 -0
  75. package/dist/types/services/core/websocket/types.d.ts +37 -0
  76. package/dist/types/services/index.d.ts +54 -0
  77. package/dist/types/services/task/AutoWrapup.d.ts +40 -0
  78. package/dist/types/services/task/TaskManager.d.ts +1 -0
  79. package/dist/types/services/task/TaskUtils.d.ts +92 -0
  80. package/dist/types/services/task/constants.d.ts +84 -0
  81. package/dist/types/services/task/contact.d.ts +69 -0
  82. package/dist/types/services/task/dialer.d.ts +43 -0
  83. package/dist/types/services/task/index.d.ts +650 -0
  84. package/dist/types/services/task/types.d.ts +1319 -0
  85. package/dist/types/types.d.ts +643 -0
  86. package/dist/types/utils/PageCache.d.ts +173 -0
  87. package/dist/types/webex-config.d.ts +53 -0
  88. package/dist/types/webex.d.ts +7 -0
  89. package/dist/types.js +14 -1
  90. package/dist/types.js.map +1 -1
  91. package/dist/webex.js +1 -1
  92. package/package.json +9 -9
  93. package/src/cc.ts +157 -30
  94. package/src/constants.ts +4 -0
  95. package/src/index.ts +1 -0
  96. package/src/metrics/behavioral-events.ts +14 -0
  97. package/src/metrics/constants.ts +11 -0
  98. package/src/services/ApiAiAssistant.ts +217 -0
  99. package/src/services/agent/types.ts +1 -1
  100. package/src/services/config/Util.ts +8 -0
  101. package/src/services/config/constants.ts +12 -0
  102. package/src/services/config/index.ts +45 -1
  103. package/src/services/config/types.ts +67 -0
  104. package/src/services/constants.ts +29 -0
  105. package/src/services/core/Err.ts +1 -0
  106. package/src/services/core/Utils.ts +32 -5
  107. package/src/services/core/aqm-reqs.ts +100 -22
  108. package/src/services/core/websocket/WebSocketManager.ts +21 -6
  109. package/src/services/core/websocket/connection-service.ts +5 -1
  110. package/src/services/index.ts +4 -0
  111. package/src/services/task/TaskManager.ts +174 -27
  112. package/src/services/task/TaskUtils.ts +12 -0
  113. package/src/services/task/constants.ts +16 -0
  114. package/src/services/task/dialer.ts +56 -1
  115. package/src/services/task/types.ts +24 -0
  116. package/src/types.ts +40 -1
  117. package/test/unit/spec/cc.ts +163 -23
  118. package/test/unit/spec/services/ApiAiAssistant.ts +115 -0
  119. package/test/unit/spec/services/config/index.ts +56 -0
  120. package/test/unit/spec/services/core/Utils.ts +63 -1
  121. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +82 -12
  122. package/test/unit/spec/services/core/websocket/connection-service.ts +3 -1
  123. package/test/unit/spec/services/task/TaskManager.ts +1119 -251
  124. package/test/unit/spec/services/task/dialer.ts +198 -112
  125. package/umd/contact-center.min.js +2 -2
  126. package/umd/contact-center.min.js.map +1 -1
@@ -15,7 +15,7 @@ import type {ContactServiceQueuesResponse} from '../../../src/types';
15
15
  import MockWebex from '@webex/test-helper-mock-webex';
16
16
  import {StationLoginSuccess, AGENT_EVENTS} from '../../../src/services/agent/types';
17
17
  import {SetStateResponse} from '../../../src/types';
18
- import {AGENT, WEB_RTC_PREFIX} from '../../../src/services/constants';
18
+ import {AGENT, SUBSCRIBE_API, WEB_RTC_PREFIX} from '../../../src/services/constants';
19
19
  import Services from '../../../src/services';
20
20
  import config from '../../../src/config';
21
21
  import {CC_EVENTS} from '../../../src/services/config/types';
@@ -88,6 +88,8 @@ describe('webex.cc', () => {
88
88
  initWebSocket: jest.fn(),
89
89
  on: jest.fn(),
90
90
  off: jest.fn(),
91
+ close: jest.fn(),
92
+ isSocketClosed: false,
91
93
  };
92
94
 
93
95
  mockContact = {
@@ -121,6 +123,13 @@ describe('webex.cc', () => {
121
123
  getOutdialAniEntries: jest.fn(),
122
124
  },
123
125
  webSocketManager: mockWebSocketManager,
126
+ rtdWebSocketManager: {
127
+ initWebSocket: jest.fn().mockResolvedValue({}),
128
+ on: jest.fn(),
129
+ off: jest.fn(),
130
+ close: jest.fn(),
131
+ isSocketClosed: false,
132
+ },
124
133
  connectionService: {
125
134
  on: jest.fn(),
126
135
  off: jest.fn(),
@@ -129,6 +138,11 @@ describe('webex.cc', () => {
129
138
 
130
139
  dialer: {
131
140
  startOutdial: jest.fn(),
141
+ acceptPreviewContact: jest.fn(),
142
+ },
143
+ apiAIAssistant: {
144
+ sendEvent: jest.fn(),
145
+ fetchHistoricTranscripts: jest.fn(),
132
146
  },
133
147
  };
134
148
 
@@ -142,6 +156,8 @@ describe('webex.cc', () => {
142
156
  setWrapupData: jest.fn(),
143
157
  setAgentId: jest.fn(),
144
158
  setWebRtcEnabled: jest.fn(),
159
+ handleRealtimeTranscriptEvent: jest.fn(),
160
+ setApiAIAssistant: jest.fn(),
145
161
  registerIncomingCallEvent: jest.fn(),
146
162
  registerTaskListeners: jest.fn(),
147
163
  getTask: jest.fn(),
@@ -262,6 +278,7 @@ describe('webex.cc', () => {
262
278
  };
263
279
 
264
280
  it('should register successfully and return agent profile', async () => {
281
+ mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: true}} as any;
265
282
  const mercuryConnect = jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
266
283
  const connectWebsocketSpy = jest.spyOn(webex.cc, 'connectWebsocket');
267
284
  const setupEventListenersSpy = jest.spyOn(webex.cc, 'setupEventListeners');
@@ -305,6 +322,7 @@ describe('webex.cc', () => {
305
322
  clientType: 'WebexCCSDK',
306
323
  allowMultiLogin: false,
307
324
  },
325
+ resource: SUBSCRIBE_API,
308
326
  });
309
327
 
310
328
  // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
@@ -317,6 +335,19 @@ describe('webex.cc', () => {
317
335
  expect.any(Function)
318
336
  );
319
337
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
338
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).toHaveBeenCalledWith({
339
+ body: {
340
+ force: true,
341
+ isKeepAliveEnabled: false,
342
+ clientType: 'WebexCCSDK',
343
+ allowMultiLogin: false,
344
+ },
345
+ resource: 'v1/realtime/subscribe',
346
+ });
347
+ expect(webex.cc.services.rtdWebSocketManager.on).toHaveBeenCalledWith(
348
+ 'message',
349
+ expect.any(Function)
350
+ );
320
351
 
321
352
  expect(configSpy).toHaveBeenCalled();
322
353
  expect(LoggerProxy.log).toHaveBeenCalledWith('Agent config is fetched successfully', {
@@ -360,6 +391,7 @@ describe('webex.cc', () => {
360
391
  clientType: 'WebexCCSDK',
361
392
  allowMultiLogin: true,
362
393
  },
394
+ resource: SUBSCRIBE_API,
363
395
  });
364
396
  expect(configSpy).toHaveBeenCalled();
365
397
  expect(LoggerProxy.log).toHaveBeenCalledWith('Agent config is fetched successfully', {
@@ -439,6 +471,7 @@ describe('webex.cc', () => {
439
471
  clientType: 'WebexCCSDK',
440
472
  allowMultiLogin: false,
441
473
  },
474
+ resource: SUBSCRIBE_API,
442
475
  });
443
476
 
444
477
  expect(mockTaskManager.on).toHaveBeenCalledWith(
@@ -462,6 +495,7 @@ describe('webex.cc', () => {
462
495
 
463
496
  it('should not attempt for mercury connection when webrtc is disabled', async () => {
464
497
  mockAgentProfile.webRtcEnabled = false;
498
+ mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: false}} as any;
465
499
  const mercurySpy = jest.spyOn(webex.internal.mercury, 'connect');
466
500
  const connectWebsocketSpy = jest.spyOn(webex.cc, 'connectWebsocket');
467
501
  const setupEventListenersSpy = jest.spyOn(webex.cc, 'setupEventListeners');
@@ -491,12 +525,39 @@ describe('webex.cc', () => {
491
525
  clientType: 'WebexCCSDK',
492
526
  allowMultiLogin: false,
493
527
  },
528
+ resource: SUBSCRIBE_API,
494
529
  });
495
530
 
496
531
  expect(configSpy).toHaveBeenCalled();
497
532
  expect(mercurySpy).not.toHaveBeenCalled();
533
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).not.toHaveBeenCalled();
498
534
  expect(result).toEqual(mockAgentProfile);
499
535
  });
536
+
537
+ it('should not connect RTD websocket when realtime transcripts feature is disabled', async () => {
538
+ mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: false}} as any;
539
+ jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
540
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
541
+ data: {
542
+ auxCodeId: 'auxCodeId',
543
+ agentId: 'agentId',
544
+ deviceType: LoginOption.EXTENSION,
545
+ dn: '12345',
546
+ },
547
+ });
548
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue(mockAgentProfile);
549
+ mockWebSocketManager.initWebSocket.mockResolvedValue({
550
+ agentId: 'agent123',
551
+ });
552
+
553
+ await webex.cc.register();
554
+
555
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).not.toHaveBeenCalled();
556
+ expect(webex.cc.services.rtdWebSocketManager.on).not.toHaveBeenCalledWith(
557
+ 'message',
558
+ expect.any(Function)
559
+ );
560
+ });
500
561
  });
501
562
 
502
563
  describe('stationLogin', () => {
@@ -1502,6 +1563,13 @@ describe('webex.cc', () => {
1502
1563
  });
1503
1564
 
1504
1565
  it('should unregister successfully and clean up all resources when webrtc is enabled', async () => {
1566
+ webex.cc.services.rtdWebSocketManager = {
1567
+ isSocketClosed: false,
1568
+ close: jest.fn(),
1569
+ off: jest.fn(),
1570
+ on: jest.fn(),
1571
+ } as any;
1572
+
1505
1573
  await webex.cc.deregister();
1506
1574
 
1507
1575
  expect(mockTaskManager.off).toHaveBeenCalledWith(
@@ -1513,12 +1581,20 @@ describe('webex.cc', () => {
1513
1581
  expect.any(Function)
1514
1582
  );
1515
1583
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1584
+ expect(webex.cc.services.rtdWebSocketManager.off).toHaveBeenCalledWith(
1585
+ 'message',
1586
+ expect.any(Function)
1587
+ );
1516
1588
  expect(webex.cc.services.connectionService.off).toHaveBeenCalledWith(
1517
1589
  'connectionLost',
1518
1590
  expect.any(Function)
1519
1591
  );
1520
1592
 
1521
1593
  expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
1594
+ expect(webex.cc.services.rtdWebSocketManager.close).toHaveBeenCalledWith(
1595
+ false,
1596
+ 'Unregistering the RTD websocket'
1597
+ );
1522
1598
  expect(webex.cc.agentConfig).toBeNull();
1523
1599
 
1524
1600
  expect(webex.internal.mercury.off).toHaveBeenCalledWith('online');
@@ -1850,7 +1926,7 @@ describe('webex.cc', () => {
1850
1926
  );
1851
1927
 
1852
1928
  expect(webex.cc.stationLogout).toHaveBeenCalledWith({
1853
- logoutReason: 'User requested agent device change',
1929
+ logoutReason: 'User requested agent profile update',
1854
1930
  });
1855
1931
  expect(webex.cc.stationLogin).toHaveBeenCalledWith({
1856
1932
  teamId: 'teamId',
@@ -1934,24 +2010,26 @@ describe('webex.cc', () => {
1934
2010
  });
1935
2011
  });
1936
2012
 
1937
- it('should throw with detailed error when loginOption equals current device type', async () => {
1938
- webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
2013
+ it('should allow update when loginOption and teamId are unchanged (e.g. dialNumber or profile refresh)', async () => {
2014
+ webex.cc.webCallingService.loginOption = LoginOption.AGENT_DN;
1939
2015
  const data = {
1940
2016
  teamId: 'teamId',
1941
- loginOption: LoginOption.BROWSER,
1942
- dialNumber: '',
2017
+ loginOption: LoginOption.AGENT_DN,
2018
+ dialNumber: '1234',
1943
2019
  };
1944
- const expectedMessage =
1945
- 'Will not proceed with device update as new Device type is same as current device type and teamId is same as current teamId';
1946
-
1947
- await expect(webex.cc.updateAgentProfile(data)).rejects.toMatchObject({
1948
- message: expectedMessage,
1949
- details: expect.objectContaining({
1950
- data: expect.objectContaining({
1951
- agentId: webex.cc.agentConfig.agentId,
1952
- reason: expectedMessage,
1953
- }),
1954
- }),
2020
+ const logoutSpy = jest.spyOn(webex.cc, 'stationLogout').mockResolvedValue({});
2021
+ const loginSpy = jest.spyOn(webex.cc, 'stationLogin').mockResolvedValue({
2022
+ type: 'AgentDeviceTypeUpdateSuccess',
2023
+ } as any);
2024
+
2025
+ await expect(webex.cc.updateAgentProfile(data)).resolves.toBeDefined();
2026
+ expect(logoutSpy).toHaveBeenCalledWith({
2027
+ logoutReason: 'User requested agent profile update',
2028
+ });
2029
+ expect(loginSpy).toHaveBeenCalledWith({
2030
+ teamId: data.teamId,
2031
+ loginOption: data.loginOption,
2032
+ dialNumber: data.dialNumber,
1955
2033
  });
1956
2034
  });
1957
2035
 
@@ -2104,7 +2182,9 @@ describe('webex.cc', () => {
2104
2182
  const detailedError = new Error('Detailed service error');
2105
2183
  getErrorDetailsSpy.mockReturnValue({error: detailedError});
2106
2184
 
2107
- await expect(webex.cc.getOutdialAniEntries(mockParams)).rejects.toThrow('Detailed service error');
2185
+ await expect(webex.cc.getOutdialAniEntries(mockParams)).rejects.toThrow(
2186
+ 'Detailed service error'
2187
+ );
2108
2188
 
2109
2189
  // Verify failure metrics are tracked
2110
2190
  expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
@@ -2128,11 +2208,7 @@ describe('webex.cc', () => {
2128
2208
  );
2129
2209
 
2130
2210
  // Verify getErrorDetails was called
2131
- expect(getErrorDetailsSpy).toHaveBeenCalledWith(
2132
- mockError,
2133
- 'getOutdialAniEntries',
2134
- CC_FILE
2135
- );
2211
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(mockError, 'getOutdialAniEntries', CC_FILE);
2136
2212
  });
2137
2213
 
2138
2214
  it('should throw error when orgId is not found', async () => {
@@ -2180,4 +2256,68 @@ describe('webex.cc', () => {
2180
2256
  );
2181
2257
  });
2182
2258
  });
2259
+
2260
+ describe('acceptPreviewContact', () => {
2261
+ const previewPayload = {
2262
+ interactionId: 'interaction-123',
2263
+ campaignId: 'campaign-456',
2264
+ };
2265
+
2266
+ it('should accept preview contact successfully', async () => {
2267
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
2268
+
2269
+ const acceptPreviewContactMock = jest
2270
+ .spyOn(webex.cc.services.dialer, 'acceptPreviewContact')
2271
+ .mockResolvedValue(mockResponse);
2272
+
2273
+ const result = await webex.cc.acceptPreviewContact(previewPayload);
2274
+
2275
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Accepting campaign preview contact', {
2276
+ module: CC_FILE,
2277
+ method: 'acceptPreviewContact',
2278
+ });
2279
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
2280
+ 'Campaign preview contact accepted successfully',
2281
+ {
2282
+ module: CC_FILE,
2283
+ method: 'acceptPreviewContact',
2284
+ trackingId: 'track-123',
2285
+ interactionId: previewPayload.interactionId,
2286
+ }
2287
+ );
2288
+
2289
+ expect(acceptPreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
2290
+ expect(result).toEqual(mockResponse);
2291
+ });
2292
+
2293
+ it('should handle error during acceptPreviewContact', async () => {
2294
+ getErrorDetailsSpy.mockRestore();
2295
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
2296
+
2297
+ const error = {
2298
+ details: {
2299
+ trackingId: '1234',
2300
+ data: {
2301
+ reason: 'Error while performing acceptPreviewContact',
2302
+ },
2303
+ },
2304
+ };
2305
+
2306
+ jest.spyOn(webex.cc.services.dialer, 'acceptPreviewContact').mockRejectedValue(error);
2307
+
2308
+ await expect(webex.cc.acceptPreviewContact(previewPayload)).rejects.toThrow(
2309
+ error.details.data.reason
2310
+ );
2311
+
2312
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Accepting campaign preview contact', {
2313
+ module: CC_FILE,
2314
+ method: 'acceptPreviewContact',
2315
+ });
2316
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
2317
+ `acceptPreviewContact failed with reason: ${error.details.data.reason}`,
2318
+ {module: CC_FILE, method: 'acceptPreviewContact', trackingId: error.details.trackingId}
2319
+ );
2320
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'acceptPreviewContact', CC_FILE);
2321
+ });
2322
+ });
2183
2323
  });
@@ -0,0 +1,115 @@
1
+ import ApiAIAssistant from '../../../../src/services/ApiAiAssistant';
2
+ import MetricsManager from '../../../../src/metrics/MetricsManager';
3
+ import LoggerProxy from '../../../../src/logger-proxy';
4
+ import {HTTP_METHODS, WebexSDK} from '../../../../src/types';
5
+
6
+ jest.mock('../../../../src/metrics/MetricsManager');
7
+ jest.mock('../../../../src/logger-proxy');
8
+
9
+ describe('ApiAIAssistant', () => {
10
+ let apiAIAssistant: ApiAIAssistant;
11
+ let mockWebex: WebexSDK;
12
+ let mockMetricsManager: jest.Mocked<MetricsManager>;
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+
17
+ mockWebex = {
18
+ credentials: {
19
+ getOrgId: jest.fn().mockReturnValue('test-org-id'),
20
+ },
21
+ request: jest.fn(),
22
+ internal: {
23
+ services: {
24
+ get: jest.fn().mockReturnValue('https://api.wxcc-us1.cisco.com'),
25
+ },
26
+ newMetrics: {
27
+ submitBehavioralEvent: jest.fn(),
28
+ submitOperationalEvent: jest.fn(),
29
+ submitBusinessEvent: jest.fn(),
30
+ },
31
+ },
32
+ ready: true,
33
+ once: jest.fn(),
34
+ } as unknown as WebexSDK;
35
+
36
+ mockMetricsManager = {
37
+ trackEvent: jest.fn(),
38
+ timeEvent: jest.fn(),
39
+ } as unknown as jest.Mocked<MetricsManager>;
40
+ (MetricsManager.getInstance as jest.Mock).mockReturnValue(mockMetricsManager);
41
+
42
+ apiAIAssistant = new ApiAIAssistant(mockWebex);
43
+ });
44
+
45
+ it('should send transcript start event successfully', async () => {
46
+ (mockWebex.request as jest.Mock).mockResolvedValue({body: {ok: true}});
47
+
48
+ const result = await apiAIAssistant.sendEvent(
49
+ 'test-agent-id',
50
+ 'interaction-1',
51
+ 'CUSTOM_EVENT',
52
+ 'GET_TRANSCRIPTS',
53
+ 'START'
54
+ );
55
+
56
+ expect(mockWebex.request).toHaveBeenCalledWith({
57
+ uri: 'https://api-ai-assistant.produs1.ciscoccservice.com/event',
58
+ method: HTTP_METHODS.POST,
59
+ addAuthHeader: true,
60
+ body: {
61
+ agentId: 'test-agent-id',
62
+ orgId: 'test-org-id',
63
+ eventType: 'CUSTOM_EVENT',
64
+ eventName: 'GET_TRANSCRIPTS',
65
+ eventDetails: {
66
+ data: expect.objectContaining({
67
+ interactionId: 'interaction-1',
68
+ action: 'START',
69
+ }),
70
+ },
71
+ },
72
+ });
73
+ expect(result).toEqual({ok: true});
74
+ });
75
+
76
+ it('should fetch historic transcripts with mapped base URL', async () => {
77
+ const responseBody = {interactionId: 'interaction-1', data: []};
78
+ (mockWebex.request as jest.Mock).mockResolvedValue({body: responseBody});
79
+ apiAIAssistant.setAIFeatureFlags({realtimeTranscripts: {enable: true}} as any);
80
+
81
+ const result = await apiAIAssistant.fetchHistoricTranscripts('test-agent-id', 'interaction-1');
82
+
83
+ expect(mockWebex.request).toHaveBeenCalledWith({
84
+ uri: 'https://api-ai-assistant.produs1.ciscoccservice.com/transcripts/list',
85
+ method: HTTP_METHODS.POST,
86
+ addAuthHeader: true,
87
+ body: {
88
+ agentId: 'test-agent-id',
89
+ orgId: 'test-org-id',
90
+ interactionId: 'interaction-1',
91
+ },
92
+ });
93
+ expect(result).toEqual(responseBody as any);
94
+ });
95
+
96
+ it('should fail when base URL mapping is not available', async () => {
97
+ (mockWebex.internal.services.get as jest.Mock).mockReturnValue('https://unknown-host.invalid');
98
+
99
+ let failed = false;
100
+ try {
101
+ await apiAIAssistant.sendEvent(
102
+ 'test-agent-id',
103
+ 'interaction-1',
104
+ 'CUSTOM_EVENT',
105
+ 'GET_TRANSCRIPTS',
106
+ 'STOP'
107
+ );
108
+ } catch (_error) {
109
+ failed = true;
110
+ }
111
+
112
+ expect(failed).toBe(true);
113
+ expect(LoggerProxy.error).toHaveBeenCalled();
114
+ });
115
+ });
@@ -490,6 +490,49 @@ describe('AgentConfigService', () => {
490
490
  });
491
491
  });
492
492
 
493
+ describe('getAIFeatureFlags', () => {
494
+ it('should return AI feature flags successfully', async () => {
495
+ const mockResponse = {
496
+ statusCode: 200,
497
+ body: {
498
+ realtimeTranscripts: {enable: true},
499
+ },
500
+ };
501
+ mockWebexRequest.request.mockResolvedValue(mockResponse);
502
+
503
+ const result = await agentConfigService.getAIFeatureFlags(mockOrgId);
504
+ expect(result).toEqual(mockResponse.body);
505
+ expect(LoggerProxy.log).toHaveBeenCalledWith('getAIFeatureFlags api success.', {
506
+ module: CONFIG_FILE_NAME,
507
+ method: 'getAIFeatureFlags',
508
+ });
509
+ });
510
+
511
+ it('should throw an error if API call returns non-200 status code', async () => {
512
+ const mockError = {statusCode: 500};
513
+ mockWebexRequest.request.mockResolvedValue(mockError);
514
+
515
+ await expect(agentConfigService.getAIFeatureFlags(mockOrgId)).rejects.toThrow(
516
+ 'API call failed with 500'
517
+ );
518
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
519
+ 'getAIFeatureFlags API call failed with Error: API call failed with 500',
520
+ {module: CONFIG_FILE_NAME, method: 'getAIFeatureFlags'}
521
+ );
522
+ });
523
+
524
+ it('should handle network errors gracefully', async () => {
525
+ const networkError = new Error('Network Error');
526
+ mockWebexRequest.request.mockRejectedValue(networkError);
527
+
528
+ await expect(agentConfigService.getAIFeatureFlags(mockOrgId)).rejects.toThrow('Network Error');
529
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
530
+ 'getAIFeatureFlags API call failed with Error: Network Error',
531
+ {module: CONFIG_FILE_NAME, method: 'getAIFeatureFlags'}
532
+ );
533
+ });
534
+ });
535
+
493
536
  describe(`getDialPlanData`, () => {
494
537
  it('should return dial plan data successfully', async () => {
495
538
  const mockResponse = {statusCode: 200, body: {data: {}}}; // Adjust data accordingly
@@ -718,6 +761,7 @@ describe('AgentConfigService', () => {
718
761
  const mockOrgInfo = {
719
762
  tenantId: 'tenant123',
720
763
  timezone: 'GMT',
764
+ environment: 'produs1',
721
765
  };
722
766
 
723
767
  const mockOrgSettings = {
@@ -754,6 +798,9 @@ describe('AgentConfigService', () => {
754
798
  {id: 'aux1', type: 'WRAP_UP_CODE', name: 'Wrap Up Code 1', isDefault: true},
755
799
  {id: 'aux2', type: 'IDLE_CODE', name: 'Idle Code 1', isDefault: true},
756
800
  ];
801
+ const mockAIFeatureFlags = {
802
+ data: [{realtimeTranscripts: {enable: true}}],
803
+ };
757
804
 
758
805
  const parseAgentConfigsSpy = jest.spyOn(util, 'parseAgentConfigs');
759
806
  agentConfigService.getUserUsingCI = jest.fn().mockResolvedValue(mockUserConfig);
@@ -762,6 +809,7 @@ describe('AgentConfigService', () => {
762
809
  agentConfigService.getSiteInfo = jest.fn().mockResolvedValue(mockSiteInfo);
763
810
  agentConfigService.getTenantData = jest.fn().mockResolvedValue(mockTenantData);
764
811
  agentConfigService.getURLMapping = jest.fn().mockResolvedValue(mockURLMapping);
812
+ agentConfigService.getAIFeatureFlags = jest.fn().mockResolvedValue(mockAIFeatureFlags);
765
813
  agentConfigService.getAllAuxCodes = jest.fn().mockResolvedValue(mockAuxCodes);
766
814
  agentConfigService.getDesktopProfileById = jest.fn().mockResolvedValue(mockAgentProfile);
767
815
  agentConfigService.getDialPlanData = jest.fn().mockResolvedValue(mockDialPlanData);
@@ -801,6 +849,7 @@ describe('AgentConfigService', () => {
801
849
  dialPlanData: mockDialPlanData,
802
850
  urlMapping: mockURLMapping,
803
851
  multimediaProfileId: mockSiteInfo.multimediaProfileId,
852
+ aiFeatureFlags: mockAIFeatureFlags,
804
853
  });
805
854
  });
806
855
 
@@ -857,6 +906,7 @@ describe('AgentConfigService', () => {
857
906
  const mockOrgInfo = {
858
907
  tenantId: 'tenant123',
859
908
  timezone: 'GMT',
909
+ environment: 'produs1',
860
910
  };
861
911
 
862
912
  const mockOrgSettings = {
@@ -894,6 +944,9 @@ describe('AgentConfigService', () => {
894
944
  {id: 'aux1', type: 'WRAP_UP_CODE', name: 'Wrap Up Code 1'},
895
945
  {id: 'aux2', type: 'IDLE_CODE', name: 'Idle Code 1'},
896
946
  ];
947
+ const mockAIFeatureFlags = {
948
+ data: [{realtimeTranscripts: {enable: true}}],
949
+ };
897
950
 
898
951
  const parseAgentConfigsSpy = jest.spyOn(util, 'parseAgentConfigs');
899
952
  agentConfigService.getUserUsingCI = jest.fn().mockResolvedValue(mockUserConfig);
@@ -902,6 +955,7 @@ describe('AgentConfigService', () => {
902
955
  agentConfigService.getSiteInfo = jest.fn().mockResolvedValue(mockSiteInfo);
903
956
  agentConfigService.getTenantData = jest.fn().mockResolvedValue(mockTenantData);
904
957
  agentConfigService.getURLMapping = jest.fn().mockResolvedValue(mockURLMapping);
958
+ agentConfigService.getAIFeatureFlags = jest.fn().mockResolvedValue(mockAIFeatureFlags);
905
959
  agentConfigService.getAllAuxCodes = jest.fn().mockResolvedValue(mockAuxCodes);
906
960
  agentConfigService.getDesktopProfileById = jest.fn().mockResolvedValue(mockAgentProfile);
907
961
  agentConfigService.getDialPlanData = jest.fn().mockResolvedValue(mockDialPlanData);
@@ -941,6 +995,7 @@ describe('AgentConfigService', () => {
941
995
  dialPlanData: mockDialPlanData,
942
996
  urlMapping: mockURLMapping,
943
997
  multimediaProfileId: mockSiteInfo.multimediaProfileId,
998
+ aiFeatureFlags: mockAIFeatureFlags,
944
999
  });
945
1000
  });
946
1001
 
@@ -952,6 +1007,7 @@ describe('AgentConfigService', () => {
952
1007
  agentConfigService.getOrganizationSetting = jest.fn().mockResolvedValue({});
953
1008
  agentConfigService.getTenantData = jest.fn().mockResolvedValue({});
954
1009
  agentConfigService.getURLMapping = jest.fn().mockResolvedValue({});
1010
+ agentConfigService.getAIFeatureFlags = jest.fn().mockResolvedValue({data: []});
955
1011
  agentConfigService.getAllAuxCodes = jest.fn().mockResolvedValue({});
956
1012
  agentConfigService.getDesktopProfileById = jest.fn().mockResolvedValue({});
957
1013
  agentConfigService.getDialPlanData = jest.fn().mockResolvedValue({});
@@ -1,4 +1,5 @@
1
1
  import * as Utils from '../../../../../src/services/core/Utils';
2
+ import {FALLBACK_DIAL_NUMBER_REGEX} from '../../../../../src/services/core/Utils';
2
3
  import LoggerProxy from '../../../../../src/logger-proxy';
3
4
  import WebexRequest from '../../../../../src/services/core/WebexRequest';
4
5
  import {LoginOption, WebexRequestPayload} from '../../../../../src/types';
@@ -244,7 +245,7 @@ describe('Utils', () => {
244
245
  const result = Utils.getStationLoginErrorData(failure, LoginOption.AGENT_DN);
245
246
  expect(result).toEqual({
246
247
  message:
247
- 'Enter a valid US dial number. For help, reach out to your administrator or support team.',
248
+ 'Enter a valid dial number. For help, reach out to your administrator or support team.',
248
249
  fieldName: LoginOption.AGENT_DN,
249
250
  });
250
251
  });
@@ -557,4 +558,65 @@ describe('Utils', () => {
557
558
  });
558
559
  });
559
560
 
561
+ describe('isValidDialNumber', () => {
562
+ const anyFormatEntry = {
563
+ name: 'Any Format',
564
+ prefix: '',
565
+ regex: '([0-9a-zA-Z]+[-._])*[0-9a-zA-Z]+',
566
+ strippedChars: '( )-',
567
+ };
568
+
569
+ const usOnlyEntry = {
570
+ name: 'US',
571
+ prefix: '1',
572
+ regex: FALLBACK_DIAL_NUMBER_REGEX.source,
573
+ strippedChars: '( )-',
574
+ };
575
+
576
+ describe('with multiple dial plan entries (Any Format + US)', () => {
577
+ const dialPlanEntries = [anyFormatEntry, usOnlyEntry];
578
+
579
+ it('should return true for a valid US phone number', () => {
580
+ const result = Utils.isValidDialNumber('12223334567', dialPlanEntries);
581
+ expect(result).toBe(true);
582
+ });
583
+
584
+ it('should return true for a UK phone number', () => {
585
+ const result = Utils.isValidDialNumber('+442030484377', dialPlanEntries);
586
+ expect(result).toBe(true);
587
+ });
588
+ });
589
+
590
+ describe('with US-only dial plan entry', () => {
591
+ const dialPlanEntries = [usOnlyEntry];
592
+
593
+ it('should return true for a valid US phone number', () => {
594
+ const result = Utils.isValidDialNumber('12223334567', dialPlanEntries);
595
+ expect(result).toBe(true);
596
+ });
597
+
598
+ it('should return false for a UK phone number', () => {
599
+ const result = Utils.isValidDialNumber('+442030484377', dialPlanEntries);
600
+ expect(result).toBe(false);
601
+ });
602
+
603
+ it('should return false for an invalid US number format', () => {
604
+ const result = Utils.isValidDialNumber('1234567890', dialPlanEntries);
605
+ expect(result).toBe(false);
606
+ });
607
+ });
608
+
609
+ describe('with empty dial plan entries (fallback to US regex)', () => {
610
+ it('should return true for a valid US phone number', () => {
611
+ const result = Utils.isValidDialNumber('12223334567', []);
612
+ expect(result).toBe(true);
613
+ });
614
+
615
+ it('should return false for a UK phone number', () => {
616
+ const result = Utils.isValidDialNumber('+442030484377', []);
617
+ expect(result).toBe(false);
618
+ });
619
+ });
620
+ });
621
+
560
622
  });