@webex/contact-center 3.12.0-next.147 → 3.12.0-next.149

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 (82) hide show
  1. package/.sdd/manifest.json +4 -2
  2. package/ai-docs/CONTRACTS.md +2 -0
  3. package/dist/cc.js +3 -1
  4. package/dist/cc.js.map +1 -1
  5. package/dist/constants.js +10 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/index.js +9 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/metrics/constants.js +9 -0
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/ApiAiAssistant.js +87 -10
  12. package/dist/services/ApiAiAssistant.js.map +1 -1
  13. package/dist/services/config/types.js +10 -4
  14. package/dist/services/config/types.js.map +1 -1
  15. package/dist/services/core/types.js.map +1 -1
  16. package/dist/services/task/Task.js +202 -1
  17. package/dist/services/task/Task.js.map +1 -1
  18. package/dist/services/task/TaskFactory.js +4 -4
  19. package/dist/services/task/TaskFactory.js.map +1 -1
  20. package/dist/services/task/TaskManager.js +113 -26
  21. package/dist/services/task/TaskManager.js.map +1 -1
  22. package/dist/services/task/TaskUtils.js +11 -1
  23. package/dist/services/task/TaskUtils.js.map +1 -1
  24. package/dist/services/task/constants.js +11 -2
  25. package/dist/services/task/constants.js.map +1 -1
  26. package/dist/services/task/digital/Digital.js +2 -2
  27. package/dist/services/task/digital/Digital.js.map +1 -1
  28. package/dist/services/task/types.js +31 -0
  29. package/dist/services/task/types.js.map +1 -1
  30. package/dist/services/task/voice/Voice.js +2 -2
  31. package/dist/services/task/voice/Voice.js.map +1 -1
  32. package/dist/services/task/voice/WebRTC.js +2 -2
  33. package/dist/services/task/voice/WebRTC.js.map +1 -1
  34. package/dist/types/constants.d.ts +9 -0
  35. package/dist/types/index.d.ts +4 -5
  36. package/dist/types/metrics/constants.d.ts +8 -0
  37. package/dist/types/services/ApiAiAssistant.d.ts +4 -2
  38. package/dist/types/services/config/types.d.ts +17 -7
  39. package/dist/types/services/task/Task.d.ts +15 -4
  40. package/dist/types/services/task/TaskFactory.d.ts +3 -2
  41. package/dist/types/services/task/TaskUtils.d.ts +6 -0
  42. package/dist/types/services/task/constants.d.ts +9 -0
  43. package/dist/types/services/task/digital/Digital.d.ts +3 -2
  44. package/dist/types/services/task/types.d.ts +109 -1
  45. package/dist/types/services/task/voice/Voice.d.ts +3 -2
  46. package/dist/types/services/task/voice/WebRTC.d.ts +3 -2
  47. package/dist/types/types.d.ts +10 -4
  48. package/dist/types.js +8 -4
  49. package/dist/types.js.map +1 -1
  50. package/dist/webex.js +1 -1
  51. package/package.json +3 -3
  52. package/src/cc.ts +4 -0
  53. package/src/constants.ts +12 -0
  54. package/src/index.ts +10 -2
  55. package/src/metrics/ai-docs/metrics-spec.md +12 -4
  56. package/src/metrics/constants.ts +10 -0
  57. package/src/services/ApiAiAssistant.ts +120 -12
  58. package/src/services/config/types.ts +10 -4
  59. package/src/services/core/types.ts +31 -1
  60. package/src/services/task/Task.ts +308 -5
  61. package/src/services/task/TaskFactory.ts +26 -5
  62. package/src/services/task/TaskManager.ts +148 -29
  63. package/src/services/task/TaskUtils.ts +13 -0
  64. package/src/services/task/ai-docs/task-spec.md +30 -5
  65. package/src/services/task/constants.ts +10 -0
  66. package/src/services/task/digital/Digital.ts +8 -3
  67. package/src/services/task/types.ts +139 -0
  68. package/src/services/task/voice/Voice.ts +8 -3
  69. package/src/services/task/voice/WebRTC.ts +6 -3
  70. package/src/types.ts +10 -4
  71. package/test/unit/fixtures/aiSummaryTestUtils.ts +40 -0
  72. package/test/unit/spec/cc.ts +1314 -32
  73. package/test/unit/spec/index.ts +881 -0
  74. package/test/unit/spec/services/ApiAiAssistant.ts +122 -6
  75. package/test/unit/spec/services/core/WebexRequest.ts +5 -6
  76. package/test/unit/spec/services/task/Task.ts +1988 -10
  77. package/test/unit/spec/services/task/TaskFactory.ts +17 -0
  78. package/test/unit/spec/services/task/TaskManager.ts +2327 -180
  79. package/test/unit/spec/services/task/TaskUtils.ts +72 -0
  80. package/test/unit/spec/services/task/voice/Voice.ts +353 -18
  81. package/umd/contact-center.min.js +2 -2
  82. package/umd/contact-center.min.js.map +1 -1
@@ -1,5 +1,8 @@
1
1
  import 'jsdom-global/register';
2
+ import EventEmitter from 'events';
2
3
  import {
4
+ AIAssistantEventName,
5
+ AIAssistantEventType,
3
6
  BuddyAgents,
4
7
  BuddyAgentsResponse,
5
8
  LoginOption,
@@ -15,10 +18,14 @@ import type {ContactServiceQueuesResponse} from '../../../src/types';
15
18
  import MockWebex from '@webex/test-helper-mock-webex';
16
19
  import {StationLoginSuccess, AGENT_EVENTS} from '../../../src/services/agent/types';
17
20
  import {SetStateResponse} from '../../../src/types';
18
- import {AGENT, SUBSCRIBE_API, WEB_RTC_PREFIX} from '../../../src/services/constants';
21
+ import {
22
+ AGENT,
23
+ SUBSCRIBE_API,
24
+ WEB_RTC_PREFIX,
25
+ } from '../../../src/services/constants';
19
26
  import Services from '../../../src/services';
20
27
  import config from '../../../src/config';
21
- import {CC_EVENTS} from '../../../src/services/config/types';
28
+ import {CC_TASK_EVENTS, CC_EVENTS} from '../../../src/services/config/types';
22
29
  import LoggerProxy from '../../../src/logger-proxy';
23
30
  import * as Utils from '../../../src/services/core/Utils';
24
31
  import {
@@ -33,11 +40,26 @@ import {
33
40
  import '../../../__mocks__/workerMock';
34
41
  import {Profile} from '../../../src/services/config/types';
35
42
  import TaskManager from '../../../src/services/task/TaskManager';
36
- import {AgentContact, TASK_EVENTS} from '../../../src/services/task/types';
43
+ import Task from '../../../src/services/task/Task';
44
+ import {
45
+ AgentContact,
46
+ AISummaryResponse,
47
+ TASK_CHANNEL_TYPE,
48
+ TASK_EVENTS,
49
+ TaskData,
50
+ } from '../../../src/services/task/types';
51
+ import {AI_SUMMARY_REQUEST_CANCELLED} from '../../../src/services/task/constants';
52
+ import type {ApiAIAssistant} from '../../../src/services/ApiAiAssistant';
53
+ import type {AIFeatureFlags} from '../../../src/types';
37
54
  import MetricsManager from '../../../src/metrics/MetricsManager';
38
55
  import {METRIC_EVENT_NAMES} from '../../../src/metrics/constants';
39
56
  import Mercury from '@webex/internal-plugin-mercury';
40
57
  import WebexRequest from '../../../src/services/core/WebexRequest';
58
+ import {
59
+ createDeferred,
60
+ flushEventLoopTurn,
61
+ flushMicrotasks,
62
+ } from '../fixtures/aiSummaryTestUtils';
41
63
 
42
64
  jest.mock('../../../src/logger-proxy', () => ({
43
65
  __esModule: true,
@@ -45,18 +67,306 @@ jest.mock('../../../src/logger-proxy', () => ({
45
67
  log: jest.fn(),
46
68
  error: jest.fn(),
47
69
  info: jest.fn(),
70
+ warn: jest.fn(),
48
71
  initialize: jest.fn(),
49
72
  },
50
73
  }));
51
74
 
75
+ jest.mock('../../../src/services', () => ({
76
+ __esModule: true,
77
+ default: {
78
+ getInstance: jest.fn(() => ({
79
+ agent: {
80
+ stationLogin: jest.fn(),
81
+ logout: jest.fn(),
82
+ reload: jest.fn(),
83
+ stateChange: jest.fn(),
84
+ buddyAgents: jest.fn(),
85
+ },
86
+ config: {
87
+ getAgentConfig: jest.fn(),
88
+ getOutdialAniEntries: jest.fn(),
89
+ },
90
+ webSocketManager: {
91
+ initWebSocket: jest.fn(),
92
+ on: jest.fn(),
93
+ off: jest.fn(),
94
+ close: jest.fn(),
95
+ isSocketClosed: false,
96
+ },
97
+ rtdWebSocketManager: {
98
+ initWebSocket: jest.fn(),
99
+ on: jest.fn(),
100
+ off: jest.fn(),
101
+ close: jest.fn(),
102
+ isSocketClosed: false,
103
+ },
104
+ connectionService: {
105
+ on: jest.fn(),
106
+ off: jest.fn(),
107
+ },
108
+ contact: {},
109
+ dialer: {
110
+ startOutdial: jest.fn(),
111
+ acceptPreviewContact: jest.fn(),
112
+ skipPreviewContact: jest.fn(),
113
+ removePreviewContact: jest.fn(),
114
+ },
115
+ })),
116
+ },
117
+ }));
118
+
119
+ jest.mock('../../../src/services/task/TaskManager', () => ({
120
+ __esModule: true,
121
+ default: {
122
+ getTaskManager: jest.fn(() => ({
123
+ on: jest.fn(),
124
+ off: jest.fn(),
125
+ emit: jest.fn(),
126
+ setConfigFlags: jest.fn(),
127
+ setWrapupData: jest.fn(),
128
+ setAgentId: jest.fn(),
129
+ setAgentName: jest.fn(),
130
+ setWebRtcEnabled: jest.fn(),
131
+ setAnswerCallOnWebexService: jest.fn(),
132
+ registerIncomingCallEvent: jest.fn(),
133
+ registerTaskListeners: jest.fn(),
134
+ unregisterIncomingCallEvent: jest.fn(),
135
+ getTask: jest.fn(),
136
+ getActiveTasks: jest.fn(),
137
+ handleRealtimeWebsocketEvent: jest.fn(),
138
+ clearAISummaryState: jest.fn(),
139
+ })),
140
+ },
141
+ }));
142
+
143
+ jest.mock('../../../src/metrics/MetricsManager', () => ({
144
+ __esModule: true,
145
+ default: {
146
+ getInstance: jest.fn(),
147
+ getCommonTrackingFieldForAQMResponse: jest.fn(() => ({})),
148
+ getCommonTrackingFieldForAQMResponseFailed: jest.fn(() => ({})),
149
+ },
150
+ }));
151
+
152
+ jest.mock('../../../src/services/core/WebexRequest', () => ({
153
+ __esModule: true,
154
+ default: {
155
+ getInstance: jest.fn(() => ({
156
+ request: jest.fn(),
157
+ uploadLogs: jest.fn(),
158
+ })),
159
+ },
160
+ }));
161
+
162
+ jest.mock('../../../src/services/ApiAiAssistant', () => {
163
+ class MockApiAIAssistant {
164
+ public sendEvent = jest.fn();
165
+ public requestAndWaitForRtd = jest.fn();
166
+ public resolveFromRtdEvent = jest.fn();
167
+ public clearAllRtdRequests = jest.fn();
168
+ public getSuggestedResponse = jest.fn();
169
+ public fetchHistoricTranscripts = jest.fn();
170
+ public requestWellnessBreak = jest.fn();
171
+ public respondToWellnessBreak = jest.fn();
172
+ public setAIFeatureFlags = jest.fn();
173
+ public setAgentId = jest.fn();
174
+ }
175
+
176
+ return {
177
+ __esModule: true,
178
+ default: MockApiAIAssistant,
179
+ ApiAIAssistant: MockApiAIAssistant,
180
+ };
181
+ });
182
+
183
+ jest.mock('../../../src/services/WebCallingService', () => {
184
+ class MockWebCallingService {
185
+ public loginOption = 'AGENT_DN';
186
+ public registerWebCallingLine = jest.fn().mockResolvedValue(undefined);
187
+ public deregisterWebCallingLine = jest.fn().mockResolvedValue(undefined);
188
+ public setLoginOption = jest.fn((loginOption: string) => {
189
+ this.loginOption = loginOption;
190
+ });
191
+ }
192
+
193
+ return {
194
+ __esModule: true,
195
+ default: MockWebCallingService,
196
+ };
197
+ });
198
+
199
+ jest.mock('../../../src/services/task/TaskFactory', () => ({
200
+ __esModule: true,
201
+ default: {
202
+ createTask: jest.fn(),
203
+ },
204
+ }));
205
+
206
+ jest.mock('../../../src/services/task/voice/WebRTC', () => ({
207
+ __esModule: true,
208
+ default: class MockWebRTC {},
209
+ }));
210
+
211
+ jest.mock('../../../src/services/task/taskDataNormalizer', () => ({
212
+ normalizeTaskData: jest.fn((data) => data),
213
+ }));
214
+
215
+ jest.mock('../../../src/services/task/state-machine', () => ({
216
+ ...jest.requireActual('../../../src/services/task/state-machine'),
217
+ createTaskStateMachine: jest.fn(
218
+ jest.requireActual('../../../src/services/task/state-machine').createTaskStateMachine
219
+ ),
220
+ }));
221
+
222
+ jest.mock('../../../src/services/task/state-machine/uiControlsComputer', () => ({
223
+ computeUIControls: jest.fn(() => ({})),
224
+ getDefaultUIControls: jest.fn(() => ({})),
225
+ haveUIControlsChanged: jest.fn(() => false),
226
+ }));
227
+
228
+ jest.mock('../../../src/services/task/AutoWrapup', () => ({
229
+ __esModule: true,
230
+ default: class MockAutoWrapup {},
231
+ }));
232
+
233
+ jest.mock('../../../src/services/EntryPoint', () => {
234
+ class MockEntryPoint {
235
+ public getEntryPoints() {
236
+ return Promise.resolve({});
237
+ }
238
+ }
239
+
240
+ return {
241
+ __esModule: true,
242
+ default: MockEntryPoint,
243
+ EntryPoint: MockEntryPoint,
244
+ };
245
+ });
246
+
247
+ jest.mock('../../../src/services/AddressBook', () => {
248
+ class MockAddressBook {
249
+ public getEntries() {
250
+ return Promise.resolve({});
251
+ }
252
+ }
253
+
254
+ return {
255
+ __esModule: true,
256
+ default: MockAddressBook,
257
+ AddressBook: MockAddressBook,
258
+ };
259
+ });
260
+
261
+ jest.mock('../../../src/services/Queue', () => {
262
+ class MockQueue {
263
+ public getQueues() {
264
+ return Promise.resolve({});
265
+ }
266
+ }
267
+
268
+ return {
269
+ __esModule: true,
270
+ default: MockQueue,
271
+ Queue: MockQueue,
272
+ };
273
+ });
274
+
52
275
  jest.mock('../../../src/services/config');
53
276
  jest.mock('../../../src/services/core/websocket/WebSocketManager');
54
277
  jest.mock('../../../src/services/core/websocket/connection-service');
55
- jest.mock('../../../src/services/WebCallingService');
56
278
  jest.mock('uuid', () => ({v4: () => 'mock-tracking-uuid'}));
57
279
 
58
280
  global.URL.createObjectURL = jest.fn(() => 'blob:http://localhost:3000/12345');
59
281
 
282
+ class EventEmitterDouble extends EventEmitter {
283
+ public isSocketClosed = false;
284
+ public initWebSocket = jest.fn().mockResolvedValue({});
285
+ public close = jest.fn(() => {
286
+ this.isSocketClosed = true;
287
+ });
288
+ public mapCallToTask = jest.fn();
289
+
290
+ public on = jest.fn((eventName: string | symbol, listener: (...args: unknown[]) => void) => {
291
+ super.on(eventName, listener);
292
+
293
+ return this;
294
+ });
295
+
296
+ public off = jest.fn((eventName: string | symbol, listener: (...args: unknown[]) => void) => {
297
+ super.off(eventName, listener);
298
+
299
+ return this;
300
+ });
301
+ }
302
+
303
+ class AISummaryLifecycleTask extends Task {
304
+ public constructor(data: TaskData, apiAIAssistant?: ApiAIAssistant, agentId = 'agent-1') {
305
+ super(
306
+ {} as any,
307
+ data,
308
+ {
309
+ channelType: TASK_CHANNEL_TYPE.VOICE,
310
+ isEndTaskEnabled: true,
311
+ isEndConsultEnabled: true,
312
+ },
313
+ undefined,
314
+ agentId,
315
+ undefined,
316
+ apiAIAssistant
317
+ );
318
+ }
319
+
320
+ public accept() {
321
+ return Promise.resolve({} as any);
322
+ }
323
+ }
324
+
325
+ const createAISummaryLifecycleTask = (
326
+ data: TaskData = createAISummaryLifecycleTaskData(),
327
+ apiAIAssistant?: ApiAIAssistant
328
+ ): AISummaryLifecycleTask => new AISummaryLifecycleTask(data, apiAIAssistant);
329
+
330
+ const createAISummaryLifecycleTaskData = (
331
+ overrides: Partial<TaskData> & {conversationId?: string} = {}
332
+ ): TaskData => {
333
+ const {
334
+ conversationId = 'conversation-1',
335
+ interaction: interactionOverrides,
336
+ ...taskOverrides
337
+ } = overrides;
338
+ const interactionId = taskOverrides.interactionId ?? 'interaction-1';
339
+
340
+ return {
341
+ mediaResourceId: 'media-resource-1',
342
+ eventType: CC_EVENTS.AGENT_CONTACT_ASSIGNED,
343
+ agentId: 'agent-1',
344
+ destAgentId: '',
345
+ trackingId: 'tracking-1',
346
+ consultMediaResourceId: '',
347
+ interactionId,
348
+ orgId: 'org-1',
349
+ owner: 'agent-1',
350
+ queueMgr: 'queue-manager-1',
351
+ type: 'telephony',
352
+ isConferencing: false,
353
+ taskId: 'task-owner-1',
354
+ ...taskOverrides,
355
+ interaction: {
356
+ interactionId,
357
+ mainInteractionId: conversationId,
358
+ mediaType: 'telephony',
359
+ media: {
360
+ [interactionId]: {
361
+ mediaResourceId: 'media-resource-1',
362
+ },
363
+ },
364
+ callProcessingDetails: {},
365
+ ...(interactionOverrides as Record<string, unknown> | undefined),
366
+ },
367
+ } as TaskData;
368
+ };
369
+
60
370
  describe('webex.cc', () => {
61
371
  let webex;
62
372
  let mockApiAIAssistant;
@@ -169,6 +479,7 @@ describe('webex.cc', () => {
169
479
  setConfigFlags: jest.fn(),
170
480
  setWrapupData: jest.fn(),
171
481
  setAgentId: jest.fn(),
482
+ setAgentName: jest.fn(),
172
483
  setWebRtcEnabled: jest.fn(),
173
484
  setAnswerCallOnWebexService: jest.fn(),
174
485
  handleRealtimeWebsocketEvent: jest.fn(),
@@ -183,6 +494,8 @@ describe('webex.cc', () => {
183
494
  off: jest.fn(),
184
495
  emit: jest.fn(),
185
496
  unregisterIncomingCallEvent: jest.fn(),
497
+ handleRealtimeWebsocketEvent: jest.fn(),
498
+ clearAISummaryState: jest.fn(),
186
499
  };
187
500
 
188
501
  mockMetricsManager = {
@@ -206,6 +519,7 @@ describe('webex.cc', () => {
206
519
 
207
520
  afterEach(() => {
208
521
  jest.clearAllMocks();
522
+ jest.restoreAllMocks();
209
523
  });
210
524
 
211
525
  it('should initialize services and logger proxy on ready event', () => {
@@ -272,7 +586,7 @@ describe('webex.cc', () => {
272
586
  });
273
587
 
274
588
  describe('register', () => {
275
- const mockAgentProfile: Profile = {
589
+ const createFreshAgentProfile = (overrides: Partial<Profile> = {}): Profile => ({
276
590
  agentId: 'agent123',
277
591
  agentMailId: '',
278
592
  agentName: 'John',
@@ -325,7 +639,10 @@ describe('webex.cc', () => {
325
639
  webRtcEnabled: true,
326
640
  lostConnectionRecoveryTimeout: 0,
327
641
  isWellnessBreakEnabled: false,
328
- };
642
+ ...overrides,
643
+ });
644
+
645
+ const mockAgentProfile: Profile = createFreshAgentProfile();
329
646
 
330
647
  it('should register successfully and return agent profile', async () => {
331
648
  mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: true}} as any;
@@ -376,7 +693,7 @@ describe('webex.cc', () => {
376
693
  resource: SUBSCRIBE_API,
377
694
  });
378
695
 
379
- // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
696
+ // SPARK-626777 tracks moving listener cleanup into the future de-register API.
380
697
  expect(mockTaskManager.on).toHaveBeenCalledWith(
381
698
  TASK_EVENTS.TASK_INCOMING,
382
699
  expect.any(Function)
@@ -426,6 +743,7 @@ describe('webex.cc', () => {
426
743
  getWxAppUsersubPublished: expect.any(Function),
427
744
  })
428
745
  );
746
+ expect(mockTaskManager.clearAISummaryState).not.toHaveBeenCalled();
429
747
  expect(reloadSpy).toHaveBeenCalled();
430
748
  expect(result).toEqual({...mockAgentProfile, agentSessionId: 'session-after-refresh'});
431
749
  expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
@@ -722,6 +1040,909 @@ describe('webex.cc', () => {
722
1040
  expect.any(Function)
723
1041
  );
724
1042
  });
1043
+
1044
+ it('should connect RTD websocket when either generated summary organization flag is enabled', async () => {
1045
+ jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
1046
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
1047
+ data: {
1048
+ auxCodeId: 'auxCodeId',
1049
+ agentId: 'agentId',
1050
+ deviceType: LoginOption.EXTENSION,
1051
+ dn: '12345',
1052
+ },
1053
+ });
1054
+ mockWebSocketManager.initWebSocket.mockResolvedValue({agentId: 'agent123'});
1055
+
1056
+ for (const generatedSummaries of [
1057
+ {wrapUpSummariesEnabled: true, consultTransferSummariesEnabled: false},
1058
+ {wrapUpSummariesEnabled: false, consultTransferSummariesEnabled: true},
1059
+ ]) {
1060
+ jest.clearAllMocks();
1061
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue({
1062
+ ...mockAgentProfile,
1063
+ aiFeature: {generatedSummaries} as any,
1064
+ });
1065
+
1066
+ await webex.cc.register();
1067
+
1068
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).toHaveBeenCalledWith({
1069
+ body: {
1070
+ force: true,
1071
+ isKeepAliveEnabled: false,
1072
+ clientType: 'WebexCCSDK',
1073
+ allowMultiLogin: false,
1074
+ },
1075
+ resource: 'v1/realtime/subscribe',
1076
+ });
1077
+ }
1078
+ });
1079
+
1080
+ it.each([
1081
+ {
1082
+ title: 'aiFeature is absent',
1083
+ profileAgentId: 'profile-agent-without-ai-feature',
1084
+ defaultDn: 'normalized-dn-without-ai-feature',
1085
+ aiFeature: undefined,
1086
+ },
1087
+ {
1088
+ title: 'aiFeature has no optional feature objects',
1089
+ profileAgentId: 'profile-agent-with-empty-ai-feature',
1090
+ defaultDn: 'normalized-dn-with-empty-ai-feature',
1091
+ aiFeature: {} as any,
1092
+ },
1093
+ ])(
1094
+ 'should tolerate absent AI feature shapes without summary-driven RTD setup when $title',
1095
+ async ({profileAgentId, defaultDn, aiFeature}) => {
1096
+ const welcomeAgentId = 'welcome-agent-for-absent-ai-feature';
1097
+ const configuredProfile = createFreshAgentProfile({
1098
+ agentId: profileAgentId,
1099
+ defaultDn,
1100
+ dn: 'pre-register-dn',
1101
+ webRtcEnabled: true,
1102
+ aiFeature,
1103
+ });
1104
+ const expectedProfile = {...configuredProfile, dn: defaultDn};
1105
+ webex.cc.$config = {...webex.cc.$config, allowAutomatedRelogin: false};
1106
+ jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
1107
+ const configSpy = jest
1108
+ .spyOn(webex.cc.services.config, 'getAgentConfig')
1109
+ .mockResolvedValue(configuredProfile);
1110
+ mockWebSocketManager.initWebSocket.mockResolvedValue({agentId: welcomeAgentId});
1111
+
1112
+ await expect(webex.cc.register()).resolves.toEqual(expectedProfile);
1113
+
1114
+ expect(configSpy).toHaveBeenCalledWith('mockOrgId', welcomeAgentId);
1115
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).not.toHaveBeenCalled();
1116
+ }
1117
+ );
1118
+
1119
+ it.each([
1120
+ {
1121
+ title: 'all generated summary organization flags are disabled',
1122
+ aiFeature: {
1123
+ realtimeTranscripts: {enable: false},
1124
+ suggestedResponses: {enable: false},
1125
+ generatedSummaries: {
1126
+ wrapUpSummariesEnabled: false,
1127
+ consultTransferSummariesEnabled: false,
1128
+ },
1129
+ } as any,
1130
+ },
1131
+ ])(
1132
+ 'should keep registration operational without summary-driven RTD setup when $title',
1133
+ async ({aiFeature}) => {
1134
+ const welcomeAgentId = 'welcome-agent-with-disabled-summaries';
1135
+ const configuredProfile = createFreshAgentProfile({
1136
+ agentId: 'profile-agent-with-disabled-summaries',
1137
+ defaultDn: 'normalized-dn-with-disabled-summaries',
1138
+ dn: 'pre-register-dn',
1139
+ webRtcEnabled: true,
1140
+ aiFeature,
1141
+ });
1142
+ const expectedProfile = {...configuredProfile, dn: configuredProfile.defaultDn};
1143
+ webex.cc.$config = {...webex.cc.$config, allowAutomatedRelogin: false};
1144
+ const mercuryConnect = jest
1145
+ .spyOn(webex.internal.mercury, 'connect')
1146
+ .mockResolvedValue(true);
1147
+ const setupEventListenersSpy = jest.spyOn(webex.cc, 'setupEventListeners');
1148
+ const configSpy = jest
1149
+ .spyOn(webex.cc.services.config, 'getAgentConfig')
1150
+ .mockResolvedValue(configuredProfile);
1151
+ mockWebSocketManager.initWebSocket.mockResolvedValue({agentId: welcomeAgentId});
1152
+
1153
+ const result = await webex.cc.register();
1154
+
1155
+ expect(mockWebSocketManager.initWebSocket).toHaveBeenCalledWith({
1156
+ body: {
1157
+ force: true,
1158
+ isKeepAliveEnabled: false,
1159
+ clientType: 'WebexCCSDK',
1160
+ allowMultiLogin: false,
1161
+ },
1162
+ resource: SUBSCRIBE_API,
1163
+ });
1164
+ expect(configSpy).toHaveBeenCalledWith('mockOrgId', welcomeAgentId);
1165
+ expect(setupEventListenersSpy).toHaveBeenCalled();
1166
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
1167
+ TASK_EVENTS.TASK_INCOMING,
1168
+ expect.any(Function)
1169
+ );
1170
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
1171
+ TASK_EVENTS.TASK_HYDRATE,
1172
+ expect.any(Function)
1173
+ );
1174
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
1175
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1176
+ expect.any(Function)
1177
+ );
1178
+ expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
1179
+ expect(mockTaskManager.setConfigFlags).toHaveBeenCalledWith({
1180
+ isEndTaskEnabled: configuredProfile.isEndTaskEnabled,
1181
+ isEndConsultEnabled: configuredProfile.isEndConsultEnabled,
1182
+ webRtcEnabled: configuredProfile.webRtcEnabled,
1183
+ autoWrapup: configuredProfile.wrapUpData.wrapUpProps.autoWrapup ?? false,
1184
+ aiFeature,
1185
+ getWxAppUsersubPublished: expect.any(Function),
1186
+ consultTransfer: {
1187
+ allowConsultToQueue: configuredProfile.allowConsultToQueue,
1188
+ accessQueue: configuredProfile.accessQueue,
1189
+ accessEntryPoint: configuredProfile.accessEntryPoint,
1190
+ accessBuddyTeam: configuredProfile.accessBuddyTeam,
1191
+ },
1192
+ enableWxBetterTogether: false,
1193
+ });
1194
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).not.toHaveBeenCalled();
1195
+ expect(webex.cc.services.rtdWebSocketManager.on).not.toHaveBeenCalledWith(
1196
+ 'message',
1197
+ expect.any(Function)
1198
+ );
1199
+ expect(mercuryConnect).toHaveBeenCalled();
1200
+ expect(result).toEqual(expectedProfile);
1201
+ }
1202
+ );
1203
+ });
1204
+
1205
+ describe('AI summary lifecycle wiring', () => {
1206
+ const sentinels = [
1207
+ 'summary-sentinel-cc-lifecycle',
1208
+ 'section-key-sentinel-cc-lifecycle',
1209
+ 'section-value-sentinel-cc-lifecycle',
1210
+ 'adaptive-card-sentinel-cc-lifecycle',
1211
+ 'agent-name-sentinel-cc-lifecycle',
1212
+ ];
1213
+
1214
+ const createSummaryHarness = () => {
1215
+ const ActualTaskManager = jest.requireActual('../../../src/services/task/TaskManager')
1216
+ .default as typeof TaskManager;
1217
+ const webSocketManager = new EventEmitterDouble();
1218
+ const rtdWebSocketManager = new EventEmitterDouble();
1219
+ const connectionService = new EventEmitterDouble();
1220
+ const webCallingService = new EventEmitterDouble();
1221
+ const transportDeferreds: Deferred<void>[] = [];
1222
+ const pendingRequests = new Map<string, any>();
1223
+ const apiAIAssistant: any = {
1224
+ aiFeature: {
1225
+ id: 'test-ai-feature',
1226
+ generatedSummaries: {
1227
+ wrapUpSummariesEnabled: true,
1228
+ consultTransferSummariesEnabled: true,
1229
+ },
1230
+ },
1231
+ getSuggestedResponse: jest.fn(),
1232
+ fetchHistoricTranscripts: jest.fn(),
1233
+ setAIFeatureFlags: jest.fn((flags: AIFeatureFlags) => {
1234
+ apiAIAssistant.aiFeature = flags;
1235
+ }),
1236
+ setAgentId: jest.fn(),
1237
+ pendingRequests,
1238
+ sendEvent: jest.fn((_agentId: string, _interactionId: string, _eventType: string, eventName: string) => {
1239
+ if (eventName.includes('SUMMARY_RESPONSE')) {
1240
+ return Promise.resolve(undefined);
1241
+ }
1242
+
1243
+ const deferred = createDeferred<void>();
1244
+
1245
+ transportDeferreds.push(deferred);
1246
+
1247
+ return deferred.promise;
1248
+ }),
1249
+ };
1250
+ apiAIAssistant.requestAndWaitForRtd = jest.fn(async (options: any) => {
1251
+ const publishTimestamp = Date.now();
1252
+ let resolveResult: (payload: any) => void = () => undefined;
1253
+ let rejectResult: (error: Error) => void = () => undefined;
1254
+ const result = new Promise((resolve, reject) => {
1255
+ resolveResult = resolve;
1256
+ rejectResult = reject;
1257
+ });
1258
+ const key = JSON.stringify([options.rtdEventType, options.correlationId]);
1259
+ pendingRequests.set(key, {
1260
+ eventType: options.rtdEventType,
1261
+ correlationId: options.correlationId,
1262
+ resolve: resolveResult,
1263
+ reject: rejectResult,
1264
+ result,
1265
+ });
1266
+ try {
1267
+ const acknowledgement = apiAIAssistant.sendEvent(
1268
+ options.agentId,
1269
+ options.interactionId,
1270
+ options.eventType,
1271
+ options.eventName,
1272
+ options.eventMetaData,
1273
+ undefined,
1274
+ undefined,
1275
+ publishTimestamp,
1276
+ options.timeout
1277
+ );
1278
+ const [payload] = await Promise.all([result, acknowledgement]);
1279
+ pendingRequests.delete(key);
1280
+ return payload;
1281
+ } catch (error) {
1282
+ pendingRequests.delete(key);
1283
+ throw error;
1284
+ }
1285
+ });
1286
+ apiAIAssistant.resolveFromRtdEvent = jest.fn(
1287
+ (eventType: string, correlationId: string, payload: any) => {
1288
+ const key = JSON.stringify([eventType, correlationId]);
1289
+ const request = pendingRequests.get(key);
1290
+ if (!request) return 'not-found';
1291
+ pendingRequests.delete(key);
1292
+ request.resolve(payload);
1293
+ return 'resolved';
1294
+ }
1295
+ );
1296
+ apiAIAssistant.clearAllRtdRequests = jest.fn(() => {
1297
+ pendingRequests.forEach((request) => {
1298
+ const error = new Error(AI_SUMMARY_REQUEST_CANCELLED) as Error & {
1299
+ data?: Record<string, unknown>;
1300
+ };
1301
+ error.data = {errorCode: AI_SUMMARY_REQUEST_CANCELLED};
1302
+ request.reject(error);
1303
+ });
1304
+ pendingRequests.clear();
1305
+ });
1306
+ const taskManager = new ActualTaskManager(
1307
+ apiAIAssistant as any,
1308
+ mockContact,
1309
+ webCallingService as any,
1310
+ webSocketManager as any,
1311
+ rtdWebSocketManager as any
1312
+ );
1313
+ const taskFactory = jest.requireMock('../../../src/services/task/TaskFactory')
1314
+ .default as {createTask: jest.Mock};
1315
+
1316
+ taskFactory.createTask.mockImplementation(
1317
+ (
1318
+ _contact,
1319
+ _webCallingService,
1320
+ taskData: TaskData,
1321
+ _configFlags,
1322
+ _wrapupData,
1323
+ _agentId,
1324
+ _agentName,
1325
+ _answerCallService,
1326
+ taskApiAIAssistant: ApiAIAssistant
1327
+ ) => createAISummaryLifecycleTask(taskData, taskApiAIAssistant)
1328
+ );
1329
+ const task = createAISummaryLifecycleTask(undefined, apiAIAssistant);
1330
+
1331
+ taskManager.setConfigFlags({
1332
+ isEndTaskEnabled: true,
1333
+ isEndConsultEnabled: true,
1334
+ webRtcEnabled: false,
1335
+ autoWrapup: false,
1336
+ aiFeature: {
1337
+ generatedSummaries: {
1338
+ wrapUpSummariesEnabled: true,
1339
+ consultTransferSummariesEnabled: true,
1340
+ },
1341
+ },
1342
+ } as any);
1343
+ taskManager.setAgentId('agent-1');
1344
+ taskManager.setWebRtcEnabled(false);
1345
+ (taskManager as any).taskCollection[task.data.interactionId] = task;
1346
+
1347
+ (webex.cc as any).taskManager = taskManager;
1348
+ webex.cc.apiAIAssistant = apiAIAssistant as any;
1349
+ webex.cc.services.webSocketManager = webSocketManager as any;
1350
+ webex.cc.services.rtdWebSocketManager = rtdWebSocketManager as any;
1351
+ webex.cc.services.connectionService = connectionService as any;
1352
+
1353
+ const emitRtdFrame = (type: string, data: Record<string, unknown>) => {
1354
+ rtdWebSocketManager.emit('message', JSON.stringify({type, data: {data}}));
1355
+ };
1356
+ const dispatchRtdFrame = (type: string, data: Record<string, unknown>) => {
1357
+ webex.cc['handleRTDWebsocketMessage'](JSON.stringify({type, data: {data}}));
1358
+ };
1359
+ const getApiAIAssistant = () => apiAIAssistant as any;
1360
+ const getSummaryMapCounts = () => {
1361
+ const api = getApiAIAssistant() as any;
1362
+ const manager = taskManager as any;
1363
+ const pendingRequests = Array.from(api.pendingRequests.values());
1364
+
1365
+ return {
1366
+ pendingPostCall: pendingRequests.filter(
1367
+ (request: any) => request.eventType === 'POST_CALL_SUMMARY'
1368
+ ).length,
1369
+ pendingMidCall: pendingRequests.filter(
1370
+ (request: any) => request.eventType === 'MID_CALL_SUMMARY'
1371
+ ).length,
1372
+ receiving: manager.receivingSummaryBuffer.size,
1373
+ featureEnablement: manager.pendingFeatureEnablement.size,
1374
+ };
1375
+ };
1376
+ const expectSummaryStateCleared = () => {
1377
+ expect(getSummaryMapCounts()).toEqual({
1378
+ pendingPostCall: 0,
1379
+ pendingMidCall: 0,
1380
+ receiving: 0,
1381
+ featureEnablement: 0,
1382
+ });
1383
+ };
1384
+
1385
+ return {
1386
+ apiAIAssistant,
1387
+ connectionService,
1388
+ dispatchRtdFrame,
1389
+ emitRtdFrame,
1390
+ expectSummaryStateCleared,
1391
+ getApiAIAssistant,
1392
+ getSummaryMapCounts,
1393
+ rtdWebSocketManager,
1394
+ task,
1395
+ taskManager,
1396
+ transportDeferreds,
1397
+ webSocketManager,
1398
+ };
1399
+ };
1400
+
1401
+ const attachRtdMessageListener = (rtdWebSocketManager: EventEmitterDouble) => {
1402
+ rtdWebSocketManager.on('message', webex.cc['handleRTDWebsocketMessage']);
1403
+ };
1404
+
1405
+ const countAISummaryRequestFinalMetrics = () =>
1406
+ mockMetricsManager.trackEvent.mock.calls.filter(([eventName]) =>
1407
+ [
1408
+ METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_SUCCESS,
1409
+ METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_FAILED,
1410
+ METRIC_EVENT_NAMES.AI_SUMMARY_GET_MID_CALL_SUCCESS,
1411
+ METRIC_EVENT_NAMES.AI_SUMMARY_GET_MID_CALL_FAILED,
1412
+ ].includes(eventName)
1413
+ ).length;
1414
+
1415
+ const expectSentinelsNotObserved = () => {
1416
+ const observedArguments = JSON.stringify([
1417
+ LoggerProxy.log.mock.calls,
1418
+ LoggerProxy.info.mock.calls,
1419
+ LoggerProxy.warn.mock.calls,
1420
+ LoggerProxy.error.mock.calls,
1421
+ mockMetricsManager.trackEvent.mock.calls,
1422
+ mockMetricsManager.timeEvent.mock.calls,
1423
+ ]);
1424
+
1425
+ sentinels.forEach((sentinel) => {
1426
+ expect(observedArguments).not.toContain(sentinel);
1427
+ });
1428
+ };
1429
+
1430
+ const emitFeatureEnablement = (
1431
+ harness: ReturnType<typeof createSummaryHarness>,
1432
+ payload = {
1433
+ interactionId: 'interaction-1',
1434
+ postCallEnabled: true,
1435
+ midCallEnabled: true,
1436
+ actionTimestamp: 1,
1437
+ }
1438
+ ) => {
1439
+ harness.emitRtdFrame(CC_TASK_EVENTS.FEATURE_ENABLEMENT, payload);
1440
+ };
1441
+
1442
+ const dispatchFeatureEnablement = (
1443
+ harness: ReturnType<typeof createSummaryHarness>,
1444
+ payload = {
1445
+ interactionId: 'interaction-1',
1446
+ postCallEnabled: true,
1447
+ midCallEnabled: true,
1448
+ actionTimestamp: 1,
1449
+ }
1450
+ ) => {
1451
+ harness.dispatchRtdFrame(CC_TASK_EVENTS.FEATURE_ENABLEMENT, payload);
1452
+ };
1453
+
1454
+ const emitTaskLifecycleEvent = (
1455
+ harness: ReturnType<typeof createSummaryHarness>,
1456
+ eventType: CC_EVENTS,
1457
+ overrides: Partial<TaskData> & {conversationId?: string}
1458
+ ) => {
1459
+ const interactionId = overrides.interactionId ?? 'interaction-1';
1460
+
1461
+ harness.webSocketManager.emit(
1462
+ 'message',
1463
+ JSON.stringify({
1464
+ data: {
1465
+ ...createAISummaryLifecycleTaskData({
1466
+ mediaResourceId: interactionId,
1467
+ taskId: `task-owner-${interactionId}`,
1468
+ ...overrides,
1469
+ }),
1470
+ type: eventType,
1471
+ },
1472
+ })
1473
+ );
1474
+ };
1475
+
1476
+ const emitSentinelPostCallSummary = (
1477
+ harness: ReturnType<typeof createSummaryHarness>,
1478
+ conversationId = 'conversation-1'
1479
+ ) => {
1480
+ harness.emitRtdFrame(CC_TASK_EVENTS.POST_CALL_SUMMARY, {
1481
+ conversationId,
1482
+ summaryText: sentinels[0],
1483
+ sections: {
1484
+ [sentinels[1]]: sentinels[2],
1485
+ initialContactReason: sentinels[2],
1486
+ },
1487
+ adaptiveCard: {
1488
+ body: [{text: sentinels[3]}],
1489
+ },
1490
+ });
1491
+ };
1492
+
1493
+ const dispatchSentinelPostCallSummary = (
1494
+ harness: ReturnType<typeof createSummaryHarness>,
1495
+ conversationId = 'conversation-1'
1496
+ ) => {
1497
+ harness.dispatchRtdFrame(CC_TASK_EVENTS.POST_CALL_SUMMARY, {
1498
+ conversationId,
1499
+ summaryText: sentinels[0],
1500
+ sections: {
1501
+ [sentinels[1]]: sentinels[2],
1502
+ initialContactReason: sentinels[2],
1503
+ },
1504
+ adaptiveCard: {
1505
+ body: [{text: sentinels[3]}],
1506
+ },
1507
+ });
1508
+ };
1509
+
1510
+ const emitSentinelMidCallSummary = (
1511
+ harness: ReturnType<typeof createSummaryHarness>,
1512
+ conversationId = 'conversation-1'
1513
+ ) => {
1514
+ harness.emitRtdFrame(CC_TASK_EVENTS.MID_CALL_SUMMARY, {
1515
+ conversationId,
1516
+ summaryText: sentinels[0],
1517
+ agentName: sentinels[4],
1518
+ sections: {
1519
+ [sentinels[1]]: sentinels[2],
1520
+ reasonForTransferOrConsult: sentinels[2],
1521
+ },
1522
+ adaptiveCard: {
1523
+ body: [{text: sentinels[3]}],
1524
+ },
1525
+ });
1526
+ };
1527
+
1528
+ afterEach(() => {
1529
+ jest.useRealTimers();
1530
+ });
1531
+
1532
+ it('forwards task-manager handlers through ContactCenter trigger and delegates RTD messages', () => {
1533
+ const triggerSpy = jest.spyOn(webex.cc, 'trigger');
1534
+ const task = {data: {interactionId: 'interaction-1'}} as any;
1535
+
1536
+ webex.cc['handleTaskHydrate'](task);
1537
+ webex.cc['handleTaskMultiLoginHydrate'](task);
1538
+ webex.cc['handleTaskMerged'](task);
1539
+ webex.cc['handleCampaignPreviewReservation'](task);
1540
+ webex.cc['handleRTDWebsocketMessage']('summary-frame');
1541
+
1542
+ expect(triggerSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_HYDRATE, task);
1543
+ expect(triggerSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
1544
+ expect(triggerSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, task);
1545
+ expect(triggerSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, task);
1546
+ expect(mockTaskManager.handleRealtimeWebsocketEvent).toHaveBeenCalledWith('summary-frame');
1547
+ });
1548
+
1549
+ it('clears active summary state on deregister', async () => {
1550
+ jest.useFakeTimers();
1551
+
1552
+ const harness = createSummaryHarness();
1553
+ const triggerSpy = jest.spyOn(webex.cc, 'trigger');
1554
+ const profile = {
1555
+ agentId: 'agent-1',
1556
+ webRtcEnabled: false,
1557
+ loginVoiceOptions: [LoginOption.EXTENSION],
1558
+ isEndTaskEnabled: true,
1559
+ isEndConsultEnabled: true,
1560
+ wrapUpData: {wrapUpProps: {autoWrapup: false}},
1561
+ aiFeature: {
1562
+ generatedSummaries: {
1563
+ wrapUpSummariesEnabled: true,
1564
+ consultTransferSummariesEnabled: true,
1565
+ },
1566
+ },
1567
+ } as any;
1568
+
1569
+ webex.cc.$config = {...webex.cc.$config, allowAutomatedRelogin: false};
1570
+ harness.webSocketManager.initWebSocket.mockResolvedValue({agentId: 'agent-1'});
1571
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue(profile);
1572
+
1573
+ await webex.cc.register();
1574
+ await flushMicrotasks();
1575
+
1576
+ expect(harness.rtdWebSocketManager.listenerCount('message')).toBe(1);
1577
+
1578
+ dispatchFeatureEnablement(harness);
1579
+ expect(harness.task.aiSummaryCapabilities).toEqual({
1580
+ midCallEnabled: true,
1581
+ postCallEnabled: true,
1582
+ });
1583
+
1584
+ dispatchFeatureEnablement(harness, {
1585
+ interactionId: 'orphan-before-deregister',
1586
+ postCallEnabled: true,
1587
+ midCallEnabled: true,
1588
+ actionTimestamp: 4,
1589
+ });
1590
+ harness.dispatchRtdFrame(CC_TASK_EVENTS.MID_CALL_SUMMARY_RESPONSE_SUBSEQUENT_AGENT, {
1591
+ conversationId: 'queued-before-deregister',
1592
+ summaryText: sentinels[0],
1593
+ });
1594
+ expect(jest.getTimerCount()).toBe(0);
1595
+ expect(harness.task.aiSummaryCapabilities).toEqual({
1596
+ midCallEnabled: true,
1597
+ postCallEnabled: true,
1598
+ });
1599
+ expect(harness.getSummaryMapCounts()).toEqual({
1600
+ pendingPostCall: 0,
1601
+ pendingMidCall: 0,
1602
+ receiving: 1,
1603
+ featureEnablement: 1,
1604
+ });
1605
+ expect(jest.getTimerCount()).toBe(0);
1606
+
1607
+ await webex.cc.deregister();
1608
+
1609
+ harness.expectSummaryStateCleared();
1610
+ expect(harness.task.aiSummaryCapabilities).toEqual({
1611
+ midCallEnabled: false,
1612
+ postCallEnabled: false,
1613
+ });
1614
+ expect(jest.getTimerCount()).toBe(0);
1615
+ expect(harness.rtdWebSocketManager.listenerCount('message')).toBe(0);
1616
+ triggerSpy.mockClear();
1617
+
1618
+ dispatchSentinelPostCallSummary(harness, 'queued-after-deregister');
1619
+
1620
+ expect(harness.getSummaryMapCounts()).toEqual({
1621
+ pendingPostCall: 0,
1622
+ pendingMidCall: 0,
1623
+ receiving: 0,
1624
+ featureEnablement: 0,
1625
+ });
1626
+ expect(triggerSpy).not.toHaveBeenCalled();
1627
+ expectSentinelsNotObserved();
1628
+ });
1629
+
1630
+ it('flushes a buffered receiving summary through real TaskManager task publication once the task is answered', () => {
1631
+ jest.useFakeTimers();
1632
+
1633
+ const harness = createSummaryHarness();
1634
+ const interactionId = 'receiver-arrival-interaction';
1635
+ const conversationId = interactionId;
1636
+ const receivingPayload = {
1637
+ conversationId,
1638
+ summaryText: sentinels[0],
1639
+ adaptiveCard: {body: [{text: sentinels[3]}]},
1640
+ };
1641
+ const receiverHandler = jest.fn();
1642
+ const observedOrder: string[] = [];
1643
+ let publishedTask: Task | undefined;
1644
+ let receiverEmitSpy: jest.SpyInstance | undefined;
1645
+
1646
+ harness.dispatchRtdFrame(CC_TASK_EVENTS.MID_CALL_SUMMARY_RESPONSE_SUBSEQUENT_AGENT, {
1647
+ ...receivingPayload,
1648
+ });
1649
+
1650
+ expect(harness.getSummaryMapCounts()).toEqual({
1651
+ pendingPostCall: 0,
1652
+ pendingMidCall: 0,
1653
+ receiving: 1,
1654
+ featureEnablement: 0,
1655
+ });
1656
+ expect(jest.getTimerCount()).toBe(0);
1657
+
1658
+ harness.taskManager.on(TASK_EVENTS.TASK_INCOMING, (task: Task) => {
1659
+ publishedTask = task;
1660
+ receiverEmitSpy = jest.spyOn(task, 'emit');
1661
+ task.on(TASK_EVENTS.TASK_ASSIGNED, () => observedOrder.push('answered'));
1662
+ task.on(TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, (payload) => {
1663
+ observedOrder.push('receiver-summary');
1664
+ receiverHandler(payload);
1665
+ });
1666
+ });
1667
+
1668
+ emitTaskLifecycleEvent(harness, CC_EVENTS.AGENT_CONTACT_RESERVED, {
1669
+ interactionId,
1670
+ conversationId,
1671
+ });
1672
+
1673
+ expect(publishedTask).toBe(harness.taskManager.getTask(interactionId));
1674
+ expect(publishedTask).toBeInstanceOf(Task);
1675
+ expect(receiverHandler).not.toHaveBeenCalled();
1676
+ expect(harness.getSummaryMapCounts().receiving).toBe(1);
1677
+
1678
+ emitTaskLifecycleEvent(harness, CC_EVENTS.AGENT_CONTACT_ASSIGNED, {
1679
+ interactionId,
1680
+ conversationId,
1681
+ });
1682
+
1683
+ expect(observedOrder).toEqual(['answered', 'receiver-summary']);
1684
+ expect(receiverHandler).toHaveBeenCalledTimes(1);
1685
+ expect(receiverHandler).toHaveBeenCalledWith(receivingPayload);
1686
+ expect(
1687
+ receiverEmitSpy?.mock.calls.filter(
1688
+ ([eventName]) => eventName === TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED
1689
+ )
1690
+ ).toHaveLength(1);
1691
+ expect(harness.getSummaryMapCounts()).toEqual({
1692
+ pendingPostCall: 0,
1693
+ pendingMidCall: 0,
1694
+ receiving: 0,
1695
+ featureEnablement: 0,
1696
+ });
1697
+ expect(jest.getTimerCount()).toBe(0);
1698
+ expectSentinelsNotObserved();
1699
+ });
1700
+
1701
+ it('resolves a real post-call request through RTD and keeps summary sentinels out of metrics', async () => {
1702
+ jest.useFakeTimers();
1703
+
1704
+ const harness = createSummaryHarness();
1705
+
1706
+ attachRtdMessageListener(harness.rtdWebSocketManager);
1707
+ emitFeatureEnablement(harness);
1708
+ jest.clearAllMocks();
1709
+
1710
+ const postCallRequest = harness.task.requestPostCallSummary();
1711
+
1712
+ await flushMicrotasks();
1713
+ expect(harness.apiAIAssistant.sendEvent).toHaveBeenCalledWith(
1714
+ 'agent-1',
1715
+ 'interaction-1',
1716
+ AIAssistantEventType.CTI_EVENT,
1717
+ AIAssistantEventName.GET_POST_CALL_SUMMARY,
1718
+ expect.objectContaining({conversationId: 'conversation-1'}),
1719
+ undefined,
1720
+ undefined,
1721
+ expect.any(Number),
1722
+ 15000
1723
+ );
1724
+ emitSentinelPostCallSummary(harness);
1725
+ harness.transportDeferreds[0].resolve(undefined);
1726
+
1727
+ await expect(postCallRequest).resolves.toMatchObject({
1728
+ conversationId: 'conversation-1',
1729
+ summaryText: sentinels[0],
1730
+ });
1731
+ expect(harness.getSummaryMapCounts()).toEqual({
1732
+ pendingPostCall: 0,
1733
+ pendingMidCall: 0,
1734
+ receiving: 0,
1735
+ featureEnablement: 0,
1736
+ });
1737
+ expect(jest.getTimerCount()).toBe(0);
1738
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
1739
+ METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_SUCCESS,
1740
+ expect.objectContaining({
1741
+ conversationId: 'conversation-1',
1742
+ interactionId: 'interaction-1',
1743
+ }),
1744
+ ['operational']
1745
+ );
1746
+ harness.taskManager.clearAISummaryState();
1747
+ harness.expectSummaryStateCleared();
1748
+ expectSentinelsNotObserved();
1749
+ });
1750
+
1751
+ it('sends post-call responses using current task data after manager cleanup', async () => {
1752
+ jest.useFakeTimers();
1753
+
1754
+ const harness = createSummaryHarness();
1755
+ const interactionId = 'post-call-cleanup-interaction';
1756
+ const conversationId = 'post-call-cleanup-conversation';
1757
+ const apiAIAssistant = harness.getApiAIAssistant();
1758
+ let applicationTask: Task | undefined;
1759
+
1760
+ harness.taskManager.on(TASK_EVENTS.TASK_INCOMING, (task: Task) => {
1761
+ if (task.data.interactionId === interactionId) {
1762
+ applicationTask = task;
1763
+ }
1764
+ });
1765
+ emitTaskLifecycleEvent(harness, CC_EVENTS.AGENT_CONTACT_RESERVED, {
1766
+ interactionId,
1767
+ conversationId,
1768
+ });
1769
+ emitTaskLifecycleEvent(harness, CC_EVENTS.AGENT_CONTACT_ASSIGNED, {
1770
+ interactionId,
1771
+ conversationId,
1772
+ });
1773
+
1774
+ expect(applicationTask).toBe(harness.taskManager.getTask(interactionId));
1775
+ const retainedTask = applicationTask as Task;
1776
+
1777
+ dispatchFeatureEnablement(harness, {
1778
+ interactionId,
1779
+ postCallEnabled: true,
1780
+ midCallEnabled: true,
1781
+ actionTimestamp: 9,
1782
+ });
1783
+
1784
+ const postCallRequest = retainedTask.requestPostCallSummary();
1785
+
1786
+ await flushMicrotasks();
1787
+ expect(postCallRequest).toBeDefined();
1788
+ expect(harness.apiAIAssistant.sendEvent).toHaveBeenCalledWith(
1789
+ 'agent-1',
1790
+ interactionId,
1791
+ AIAssistantEventType.CTI_EVENT,
1792
+ AIAssistantEventName.GET_POST_CALL_SUMMARY,
1793
+ expect.objectContaining({conversationId}),
1794
+ undefined,
1795
+ undefined,
1796
+ expect.any(Number),
1797
+ 15000
1798
+ );
1799
+ harness.dispatchRtdFrame(CC_TASK_EVENTS.POST_CALL_SUMMARY, {
1800
+ conversationId,
1801
+ summaryText: sentinels[0],
1802
+ });
1803
+ harness.transportDeferreds[0].resolve(undefined);
1804
+
1805
+ await expect(postCallRequest).resolves.toMatchObject({
1806
+ conversationId,
1807
+ summaryText: sentinels[0],
1808
+ });
1809
+ expect(harness.getSummaryMapCounts()).toEqual({
1810
+ pendingPostCall: 0,
1811
+ pendingMidCall: 0,
1812
+ receiving: 0,
1813
+ featureEnablement: 0,
1814
+ });
1815
+
1816
+ emitTaskLifecycleEvent(harness, CC_EVENTS.AGENT_WRAPUP, {
1817
+ interactionId,
1818
+ conversationId,
1819
+ });
1820
+ emitTaskLifecycleEvent(harness, CC_EVENTS.AGENT_WRAPPEDUP, {
1821
+ interactionId,
1822
+ conversationId,
1823
+ });
1824
+
1825
+ expect(harness.taskManager.getTask(interactionId)).toBeUndefined();
1826
+ expect(harness.getSummaryMapCounts()).toEqual({
1827
+ pendingPostCall: 0,
1828
+ pendingMidCall: 0,
1829
+ receiving: 0,
1830
+ featureEnablement: 0,
1831
+ });
1832
+ expect(jest.getTimerCount()).toBe(0);
1833
+
1834
+ const requestCallsAfterRemoval = (apiAIAssistant as any).pendingRequests.size;
1835
+ const responseInteractionId = 'post-call-response-current-interaction';
1836
+ const responseConversationId = 'post-call-response-current-conversation';
1837
+
1838
+ retainedTask.updateTaskData(
1839
+ createAISummaryLifecycleTaskData({
1840
+ interactionId: responseInteractionId,
1841
+ conversationId: responseConversationId,
1842
+ }),
1843
+ true
1844
+ );
1845
+
1846
+ const responsePayload: AISummaryResponse = {
1847
+ summary: {initialContactReason: 'resolved'},
1848
+ feedback: 'thumbs_up',
1849
+ state: 'DEFAULT',
1850
+ wrapUpCode: 'resolved',
1851
+ numberOfTimesViewed: 1,
1852
+ numberOfTimesEdited: 0,
1853
+ numberOfTimesCopied: 0,
1854
+ };
1855
+
1856
+ await expect(
1857
+ retainedTask.sendPostCallSummaryResponse(responsePayload)
1858
+ ).resolves.toBeUndefined();
1859
+
1860
+ expect((apiAIAssistant as any).pendingRequests.size).toBe(requestCallsAfterRemoval);
1861
+ expect(harness.apiAIAssistant.sendEvent).toHaveBeenCalledTimes(2);
1862
+ expect(harness.apiAIAssistant.sendEvent).toHaveBeenNthCalledWith(
1863
+ 2,
1864
+ 'agent-1',
1865
+ responseInteractionId,
1866
+ AIAssistantEventType.CTI_EVENT,
1867
+ AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
1868
+ {
1869
+ conversationId: responseConversationId,
1870
+ clientType: 'WxCC',
1871
+ action: AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
1872
+ summary: {initialContactReason: 'resolved'},
1873
+ feedback: 'thumbs_up',
1874
+ wrapUpCode: 'resolved',
1875
+ numberOfTimesViewed: 1,
1876
+ numberOfTimesEdited: 0,
1877
+ numberOfTimesCopied: 0,
1878
+ state: 'DEFAULT',
1879
+ },
1880
+ undefined,
1881
+ undefined,
1882
+ expect.any(Number),
1883
+ 15000
1884
+ );
1885
+ });
1886
+
1887
+ it('cancels pending real public requests on deregister and ignores late HTTP settlement', async () => {
1888
+ const harness = createSummaryHarness();
1889
+ const unhandledRejections: unknown[] = [];
1890
+ const unhandledRejectionListener = jest.fn((reason) => {
1891
+ unhandledRejections.push(reason);
1892
+ });
1893
+
1894
+ attachRtdMessageListener(harness.rtdWebSocketManager);
1895
+ emitFeatureEnablement(harness);
1896
+ jest.clearAllMocks();
1897
+
1898
+ webex.cc.agentConfig = {
1899
+ agentId: 'agent-1',
1900
+ webRtcEnabled: false,
1901
+ loginVoiceOptions: [LoginOption.EXTENSION],
1902
+ };
1903
+
1904
+ process.on('unhandledRejection', unhandledRejectionListener);
1905
+ try {
1906
+ const postCallRequest = harness.task.requestPostCallSummary();
1907
+ const midCallRequest = harness.task.requestMidCallSummary('CONSULT');
1908
+ postCallRequest.catch(() => undefined);
1909
+ midCallRequest.catch(() => undefined);
1910
+
1911
+ await flushMicrotasks();
1912
+ expect(harness.transportDeferreds).toHaveLength(2);
1913
+
1914
+ await webex.cc.deregister();
1915
+ await flushEventLoopTurn();
1916
+
1917
+ expect(unhandledRejectionListener).not.toHaveBeenCalled();
1918
+ expect(unhandledRejections).toHaveLength(0);
1919
+ await expect(postCallRequest).rejects.toMatchObject({
1920
+ message: AI_SUMMARY_REQUEST_CANCELLED,
1921
+ data: {errorCode: AI_SUMMARY_REQUEST_CANCELLED},
1922
+ });
1923
+ await expect(midCallRequest).rejects.toMatchObject({
1924
+ message: AI_SUMMARY_REQUEST_CANCELLED,
1925
+ data: {errorCode: AI_SUMMARY_REQUEST_CANCELLED},
1926
+ });
1927
+ harness.expectSummaryStateCleared();
1928
+
1929
+ const finalMetricCountAfterCancellation = countAISummaryRequestFinalMetrics();
1930
+
1931
+ dispatchSentinelPostCallSummary(harness, 'queued-after-deregister');
1932
+ harness.transportDeferreds[0].resolve(undefined);
1933
+ harness.transportDeferreds[1].reject(new Error('late-http-rejected-after-deregister'));
1934
+ await flushEventLoopTurn();
1935
+
1936
+ expect(unhandledRejectionListener).not.toHaveBeenCalled();
1937
+ expect(unhandledRejections).toHaveLength(0);
1938
+ expect(countAISummaryRequestFinalMetrics()).toBe(finalMetricCountAfterCancellation);
1939
+ harness.expectSummaryStateCleared();
1940
+ expectSentinelsNotObserved();
1941
+ } finally {
1942
+ process.off('unhandledRejection', unhandledRejectionListener);
1943
+ harness.taskManager.clearAISummaryState();
1944
+ }
1945
+ });
725
1946
  });
726
1947
 
727
1948
  describe('stationLogin', () => {
@@ -1861,7 +3082,7 @@ describe('webex.cc', () => {
1861
3082
  });
1862
3083
 
1863
3084
  expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
1864
- // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
3085
+ // SPARK-626777 tracks moving listener cleanup into the future de-register API.
1865
3086
  // expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
1866
3087
  // expect(mockTaskManager.off).toHaveBeenCalledWith(
1867
3088
  // TASK_EVENTS.TASK_INCOMING,
@@ -2662,8 +3883,6 @@ describe('webex.cc', () => {
2662
3883
  );
2663
3884
 
2664
3885
  const setLoginOptionSpy = jest.spyOn(webex.cc.webCallingService, 'setLoginOption');
2665
- const incomingTaskListenerSpy = jest.spyOn(webex.cc, 'incomingTaskListener');
2666
- const webSocketManagerOnSpy = jest.spyOn(webex.cc.services.webSocketManager, 'on');
2667
3886
  await webex.cc['silentRelogin']();
2668
3887
 
2669
3888
  expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
@@ -2694,8 +3913,8 @@ describe('webex.cc', () => {
2694
3913
  expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.BROWSER);
2695
3914
  expect(registerWebCallingLineSpy).toHaveBeenCalled();
2696
3915
  expect(setLoginOptionSpy).toHaveBeenCalledWith(LoginOption.BROWSER);
2697
- // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
2698
- // expect(incomingTaskListenerSpy).toHaveBeenCalled();
3916
+ // SPARK-626777 tracks moving listener cleanup into the future de-register API.
3917
+ // expect(eventForwarderRefreshSpy).toHaveBeenCalled();
2699
3918
  // expect(webSocketManagerOnSpy).toHaveBeenCalledWith('message', expect.any(Function));
2700
3919
  // expect(mockTaskManager.on).toHaveBeenCalledWith(
2701
3920
  // TASK_EVENTS.TASK_HYDRATE,
@@ -3030,6 +4249,9 @@ describe('webex.cc', () => {
3030
4249
  {module: CC_FILE, method: `startOutdial`, trackingId: error.details.trackingId}
3031
4250
  );
3032
4251
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'startOutdial', CC_FILE);
4252
+ expect(mockWebexRequest.uploadLogs).toHaveBeenCalledWith({
4253
+ correlationId: error.details.trackingId,
4254
+ });
3033
4255
  });
3034
4256
  });
3035
4257
 
@@ -3124,6 +4346,13 @@ describe('webex.cc', () => {
3124
4346
  deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
3125
4347
  });
3126
4348
 
4349
+ afterEach(() => {
4350
+ mockTaskManager.off.mockImplementation(() => undefined);
4351
+ mockTaskManager.clearAISummaryState.mockImplementation(() => undefined);
4352
+ mockRTDWebSocketManager.off.mockImplementation(() => undefined);
4353
+ mockRTDWebSocketManager.close.mockImplementation(() => undefined);
4354
+ });
4355
+
3127
4356
  it('invalidates wellness context before deregistration cleanup can fail', async () => {
3128
4357
  const cleanupError = new Error('cleanup failed');
3129
4358
  let rejectCleanup: (error: Error) => void = () => undefined;
@@ -3136,6 +4365,9 @@ describe('webex.cc', () => {
3136
4365
  };
3137
4366
  webex.cc['updateWellnessSession']('session-1');
3138
4367
  jest.spyOn(webex.cc as any, 'teardownWxAppLocalState').mockReturnValue(cleanup);
4368
+ webex.cc.apiAIAssistant.requestWellnessBreak.mockRejectedValue(
4369
+ new Error('WELLNESS_BREAK_AGENT_SESSION_REQUIRED')
4370
+ );
3139
4371
 
3140
4372
  const deregistration = webex.cc.deregister();
3141
4373
  const rejection = expect(deregistration).rejects.toThrow(cleanupError);
@@ -3195,10 +4427,17 @@ describe('webex.cc', () => {
3195
4427
  );
3196
4428
 
3197
4429
  expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
3198
- expect(webex.cc.services.rtdWebSocketManager.close).toHaveBeenCalledWith(
4430
+ expect(webex.cc.services.rtdWebSocketManager.close).toHaveBeenNthCalledWith(
4431
+ 1,
4432
+ false,
4433
+ 'Unregistering the SDK'
4434
+ );
4435
+ expect(webex.cc.services.rtdWebSocketManager.close).toHaveBeenNthCalledWith(
4436
+ 2,
3199
4437
  false,
3200
4438
  'Unregistering the RTD websocket'
3201
4439
  );
4440
+ expect(webex.cc.services.rtdWebSocketManager.close).toHaveBeenCalledTimes(2);
3202
4441
  expect(webex.cc.agentConfig).toBeNull();
3203
4442
 
3204
4443
  expect(webex.internal.mercury.off).toHaveBeenCalledWith('online');
@@ -3210,6 +4449,7 @@ describe('webex.cc', () => {
3210
4449
  METRIC_EVENT_NAMES.WEBSOCKET_DEREGISTER_SUCCESS,
3211
4450
  METRIC_EVENT_NAMES.WEBSOCKET_DEREGISTER_FAIL,
3212
4451
  ]);
4452
+ expect(mockTaskManager.clearAISummaryState).toHaveBeenCalled();
3213
4453
  expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
3214
4454
  METRIC_EVENT_NAMES.WEBSOCKET_DEREGISTER_SUCCESS,
3215
4455
  {},
@@ -3229,22 +4469,23 @@ describe('webex.cc', () => {
3229
4469
  const incomingCalls = mockTaskManager.off.mock.calls.filter(
3230
4470
  ([evt]) => evt === TASK_EVENTS.TASK_INCOMING
3231
4471
  );
3232
- expect(incomingCalls).toHaveLength(1);
3233
- const [, incomingCallback] = incomingCalls[0];
4472
+ expect(incomingCalls.length).toBeGreaterThanOrEqual(1);
4473
+ const [, incomingCallback] = incomingCalls[incomingCalls.length - 1];
3234
4474
  expect(incomingCallback).toBe(webex.cc['handleIncomingTask']);
3235
4475
 
3236
4476
  const hydrateCalls = mockTaskManager.off.mock.calls.filter(
3237
4477
  ([evt]) => evt === TASK_EVENTS.TASK_HYDRATE
3238
4478
  );
3239
- expect(hydrateCalls).toHaveLength(1);
3240
- const [, hydrateCallback] = hydrateCalls[0];
4479
+ expect(hydrateCalls.length).toBeGreaterThanOrEqual(1);
4480
+ const [, hydrateCallback] = hydrateCalls[hydrateCalls.length - 1];
3241
4481
  expect(hydrateCallback).toBe(webex.cc['handleTaskHydrate']);
3242
4482
 
3243
4483
  const multiLoginHydrateCalls = mockTaskManager.off.mock.calls.filter(
3244
4484
  ([evt]) => evt === TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE
3245
4485
  );
3246
- expect(multiLoginHydrateCalls).toHaveLength(1);
3247
- const [, multiLoginHydrateCallback] = multiLoginHydrateCalls[0];
4486
+ expect(multiLoginHydrateCalls.length).toBeGreaterThanOrEqual(1);
4487
+ const [, multiLoginHydrateCallback] =
4488
+ multiLoginHydrateCalls[multiLoginHydrateCalls.length - 1];
3248
4489
  expect(multiLoginHydrateCallback).toBe(webex.cc['handleTaskMultiLoginHydrate']);
3249
4490
 
3250
4491
  const messageCalls = mockWebSocketManager.off.mock.calls.filter(([evt]) => evt === 'message');
@@ -3318,7 +4559,17 @@ describe('webex.cc', () => {
3318
4559
  expect(webex.internal.mercury.off).not.toHaveBeenCalled();
3319
4560
  expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
3320
4561
  expect(deviceUnregisterSpy).not.toHaveBeenCalled();
3321
- expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
4562
+ expect(mockRTDWebSocketManager.close).toHaveBeenNthCalledWith(
4563
+ 1,
4564
+ false,
4565
+ 'Unregistering the SDK'
4566
+ );
4567
+ expect(mockRTDWebSocketManager.close).toHaveBeenNthCalledWith(
4568
+ 2,
4569
+ false,
4570
+ 'Unregistering the RTD websocket'
4571
+ );
4572
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledTimes(2);
3322
4573
  });
3323
4574
 
3324
4575
  it('should skip internal mercury cleanup when loginVoiceOptions does not include BROWSER', async () => {
@@ -3336,7 +4587,17 @@ describe('webex.cc', () => {
3336
4587
  expect(deviceUnregisterSpy).not.toHaveBeenCalled();
3337
4588
 
3338
4589
  expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
3339
- expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
4590
+ expect(mockRTDWebSocketManager.close).toHaveBeenNthCalledWith(
4591
+ 1,
4592
+ false,
4593
+ 'Unregistering the SDK'
4594
+ );
4595
+ expect(mockRTDWebSocketManager.close).toHaveBeenNthCalledWith(
4596
+ 2,
4597
+ false,
4598
+ 'Unregistering the RTD websocket'
4599
+ );
4600
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledTimes(2);
3340
4601
  expect(webex.cc.agentConfig).toBeNull();
3341
4602
  });
3342
4603
 
@@ -3492,6 +4753,11 @@ describe('webex.cc', () => {
3492
4753
  module: CC_FILE,
3493
4754
  method: 'deregister',
3494
4755
  });
4756
+ expect(mockTaskManager.clearAISummaryState).toHaveBeenCalled();
4757
+ expect(webex.cc.services.rtdWebSocketManager.off).toHaveBeenCalledWith(
4758
+ 'message',
4759
+ webex.cc['handleRTDWebsocketMessage']
4760
+ );
3495
4761
 
3496
4762
  expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
3497
4763
  METRIC_EVENT_NAMES.WEBSOCKET_DEREGISTER_FAIL,
@@ -4004,16 +5270,15 @@ describe('webex.cc', () => {
4004
5270
  mockError.details = {
4005
5271
  trackingId: 'test-tracking-id',
4006
5272
  orgId: 'mockOrgId',
4007
- error: 'Service error',
5273
+ type: 'OutdialAniEntriesFailed',
5274
+ data: {
5275
+ reason: 'Detailed service error',
5276
+ },
4008
5277
  };
4009
5278
 
4010
5279
  // Mock the service call to throw an error
4011
5280
  webex.cc.services.config.getOutdialAniEntries.mockRejectedValue(mockError);
4012
5281
 
4013
- // Mock getErrorDetails to return a detailed error
4014
- const detailedError = new Error('Detailed service error');
4015
- getErrorDetailsSpy.mockReturnValue({error: detailedError});
4016
-
4017
5282
  await expect(webex.cc.getOutdialAniEntries(mockParams)).rejects.toThrow(
4018
5283
  'Detailed service error'
4019
5284
  );
@@ -4038,9 +5303,20 @@ describe('webex.cc', () => {
4038
5303
  trackingId: 'test-tracking-id',
4039
5304
  }
4040
5305
  );
5306
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
5307
+ 'getOutdialAniEntries failed with reason: Detailed service error',
5308
+ {
5309
+ module: CC_FILE,
5310
+ method: 'getOutdialAniEntries',
5311
+ trackingId: 'test-tracking-id',
5312
+ }
5313
+ );
4041
5314
 
4042
5315
  // Verify getErrorDetails was called
4043
5316
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(mockError, 'getOutdialAniEntries', CC_FILE);
5317
+ expect(mockWebexRequest.uploadLogs).toHaveBeenCalledWith({
5318
+ correlationId: 'test-tracking-id',
5319
+ });
4044
5320
  });
4045
5321
 
4046
5322
  it('should throw error when orgId is not found', async () => {
@@ -4123,8 +5399,7 @@ describe('webex.cc', () => {
4123
5399
  });
4124
5400
 
4125
5401
  it('should handle error during acceptPreviewContact', async () => {
4126
- getErrorDetailsSpy.mockRestore();
4127
- getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
5402
+ getErrorDetailsSpy.mockClear();
4128
5403
 
4129
5404
  const error = {
4130
5405
  details: {
@@ -4150,6 +5425,9 @@ describe('webex.cc', () => {
4150
5425
  {module: CC_FILE, method: 'acceptPreviewContact', trackingId: error.details.trackingId}
4151
5426
  );
4152
5427
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'acceptPreviewContact', CC_FILE);
5428
+ expect(mockWebexRequest.uploadLogs).toHaveBeenCalledWith({
5429
+ correlationId: error.details.trackingId,
5430
+ });
4153
5431
  });
4154
5432
  });
4155
5433
 
@@ -4187,8 +5465,7 @@ describe('webex.cc', () => {
4187
5465
  });
4188
5466
 
4189
5467
  it('should handle error during skipPreviewContact', async () => {
4190
- getErrorDetailsSpy.mockRestore();
4191
- getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
5468
+ getErrorDetailsSpy.mockClear();
4192
5469
 
4193
5470
  const error = {
4194
5471
  details: {
@@ -4214,6 +5491,9 @@ describe('webex.cc', () => {
4214
5491
  {module: CC_FILE, method: 'skipPreviewContact', trackingId: error.details.trackingId}
4215
5492
  );
4216
5493
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'skipPreviewContact', CC_FILE);
5494
+ expect(mockWebexRequest.uploadLogs).toHaveBeenCalledWith({
5495
+ correlationId: error.details.trackingId,
5496
+ });
4217
5497
  });
4218
5498
  });
4219
5499
 
@@ -4251,8 +5531,7 @@ describe('webex.cc', () => {
4251
5531
  });
4252
5532
 
4253
5533
  it('should handle error during removePreviewContact', async () => {
4254
- getErrorDetailsSpy.mockRestore();
4255
- getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
5534
+ getErrorDetailsSpy.mockClear();
4256
5535
 
4257
5536
  const error = {
4258
5537
  details: {
@@ -4278,6 +5557,9 @@ describe('webex.cc', () => {
4278
5557
  {module: CC_FILE, method: 'removePreviewContact', trackingId: error.details.trackingId}
4279
5558
  );
4280
5559
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'removePreviewContact', CC_FILE);
5560
+ expect(mockWebexRequest.uploadLogs).toHaveBeenCalledWith({
5561
+ correlationId: error.details.trackingId,
5562
+ });
4281
5563
  });
4282
5564
  });
4283
5565