@webex/contact-center 3.12.0-next.2 → 3.12.0-next.21

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 (52) hide show
  1. package/dist/cc.js +132 -0
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +2 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/metrics/behavioral-events.js +26 -0
  6. package/dist/metrics/behavioral-events.js.map +1 -1
  7. package/dist/metrics/constants.js +4 -0
  8. package/dist/metrics/constants.js.map +1 -1
  9. package/dist/services/config/constants.js +1 -1
  10. package/dist/services/config/constants.js.map +1 -1
  11. package/dist/services/config/types.js +4 -0
  12. package/dist/services/config/types.js.map +1 -1
  13. package/dist/services/core/Err.js.map +1 -1
  14. package/dist/services/task/TaskManager.js +90 -8
  15. package/dist/services/task/TaskManager.js.map +1 -1
  16. package/dist/services/task/constants.js +3 -1
  17. package/dist/services/task/constants.js.map +1 -1
  18. package/dist/services/task/dialer.js +78 -0
  19. package/dist/services/task/dialer.js.map +1 -1
  20. package/dist/services/task/index.js +7 -2
  21. package/dist/services/task/index.js.map +1 -1
  22. package/dist/services/task/types.js +44 -0
  23. package/dist/services/task/types.js.map +1 -1
  24. package/dist/types/cc.d.ts +44 -0
  25. package/dist/types/constants.d.ts +2 -0
  26. package/dist/types/metrics/constants.d.ts +4 -0
  27. package/dist/types/services/config/types.d.ts +8 -0
  28. package/dist/types/services/core/Err.d.ts +4 -0
  29. package/dist/types/services/task/constants.d.ts +2 -0
  30. package/dist/types/services/task/dialer.d.ts +30 -0
  31. package/dist/types/services/task/types.d.ts +53 -1
  32. package/dist/webex.js +1 -1
  33. package/package.json +8 -8
  34. package/src/cc.ts +160 -0
  35. package/src/constants.ts +2 -0
  36. package/src/metrics/behavioral-events.ts +28 -0
  37. package/src/metrics/constants.ts +4 -0
  38. package/src/services/config/constants.ts +1 -1
  39. package/src/services/config/types.ts +4 -0
  40. package/src/services/core/Err.ts +2 -0
  41. package/src/services/task/TaskManager.ts +102 -22
  42. package/src/services/task/constants.ts +2 -0
  43. package/src/services/task/dialer.ts +80 -0
  44. package/src/services/task/index.ts +7 -2
  45. package/src/services/task/types.ts +56 -0
  46. package/test/unit/spec/cc.ts +130 -0
  47. package/test/unit/spec/services/config/index.ts +1 -1
  48. package/test/unit/spec/services/task/TaskManager.ts +238 -7
  49. package/test/unit/spec/services/task/dialer.ts +190 -0
  50. package/test/unit/spec/services/task/index.ts +21 -0
  51. package/umd/contact-center.min.js +2 -2
  52. package/umd/contact-center.min.js.map +1 -1
@@ -139,6 +139,8 @@ describe('webex.cc', () => {
139
139
  dialer: {
140
140
  startOutdial: jest.fn(),
141
141
  acceptPreviewContact: jest.fn(),
142
+ skipPreviewContact: jest.fn(),
143
+ removePreviewContact: jest.fn(),
142
144
  },
143
145
  apiAIAssistant: {
144
146
  sendEvent: jest.fn(),
@@ -2320,4 +2322,132 @@ describe('webex.cc', () => {
2320
2322
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'acceptPreviewContact', CC_FILE);
2321
2323
  });
2322
2324
  });
2325
+
2326
+ describe('skipPreviewContact', () => {
2327
+ const previewPayload = {
2328
+ interactionId: 'interaction-123',
2329
+ campaignId: 'campaign-456',
2330
+ };
2331
+
2332
+ it('should skip preview contact successfully', async () => {
2333
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
2334
+
2335
+ const skipPreviewContactMock = jest
2336
+ .spyOn(webex.cc.services.dialer, 'skipPreviewContact')
2337
+ .mockResolvedValue(mockResponse);
2338
+
2339
+ const result = await webex.cc.skipPreviewContact(previewPayload);
2340
+
2341
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
2342
+ module: CC_FILE,
2343
+ method: 'skipPreviewContact',
2344
+ });
2345
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
2346
+ 'Campaign preview contact skipped successfully',
2347
+ {
2348
+ module: CC_FILE,
2349
+ method: 'skipPreviewContact',
2350
+ trackingId: 'track-123',
2351
+ interactionId: previewPayload.interactionId,
2352
+ }
2353
+ );
2354
+
2355
+ expect(skipPreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
2356
+ expect(result).toEqual(mockResponse);
2357
+ });
2358
+
2359
+ it('should handle error during skipPreviewContact', async () => {
2360
+ getErrorDetailsSpy.mockRestore();
2361
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
2362
+
2363
+ const error = {
2364
+ details: {
2365
+ trackingId: '1234',
2366
+ data: {
2367
+ reason: 'Error while performing skipPreviewContact',
2368
+ },
2369
+ },
2370
+ };
2371
+
2372
+ jest.spyOn(webex.cc.services.dialer, 'skipPreviewContact').mockRejectedValue(error);
2373
+
2374
+ await expect(webex.cc.skipPreviewContact(previewPayload)).rejects.toThrow(
2375
+ error.details.data.reason
2376
+ );
2377
+
2378
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
2379
+ module: CC_FILE,
2380
+ method: 'skipPreviewContact',
2381
+ });
2382
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
2383
+ `skipPreviewContact failed with reason: ${error.details.data.reason}`,
2384
+ {module: CC_FILE, method: 'skipPreviewContact', trackingId: error.details.trackingId}
2385
+ );
2386
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'skipPreviewContact', CC_FILE);
2387
+ });
2388
+ });
2389
+
2390
+ describe('removePreviewContact', () => {
2391
+ const previewPayload = {
2392
+ interactionId: 'interaction-123',
2393
+ campaignId: 'campaign-456',
2394
+ };
2395
+
2396
+ it('should remove preview contact successfully', async () => {
2397
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
2398
+
2399
+ const removePreviewContactMock = jest
2400
+ .spyOn(webex.cc.services.dialer, 'removePreviewContact')
2401
+ .mockResolvedValue(mockResponse);
2402
+
2403
+ const result = await webex.cc.removePreviewContact(previewPayload);
2404
+
2405
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
2406
+ module: CC_FILE,
2407
+ method: 'removePreviewContact',
2408
+ });
2409
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
2410
+ 'Campaign preview contact removed successfully',
2411
+ {
2412
+ module: CC_FILE,
2413
+ method: 'removePreviewContact',
2414
+ trackingId: 'track-123',
2415
+ interactionId: previewPayload.interactionId,
2416
+ }
2417
+ );
2418
+
2419
+ expect(removePreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
2420
+ expect(result).toEqual(mockResponse);
2421
+ });
2422
+
2423
+ it('should handle error during removePreviewContact', async () => {
2424
+ getErrorDetailsSpy.mockRestore();
2425
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
2426
+
2427
+ const error = {
2428
+ details: {
2429
+ trackingId: '1234',
2430
+ data: {
2431
+ reason: 'Error while performing removePreviewContact',
2432
+ },
2433
+ },
2434
+ };
2435
+
2436
+ jest.spyOn(webex.cc.services.dialer, 'removePreviewContact').mockRejectedValue(error);
2437
+
2438
+ await expect(webex.cc.removePreviewContact(previewPayload)).rejects.toThrow(
2439
+ error.details.data.reason
2440
+ );
2441
+
2442
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
2443
+ module: CC_FILE,
2444
+ method: 'removePreviewContact',
2445
+ });
2446
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
2447
+ `removePreviewContact failed with reason: ${error.details.data.reason}`,
2448
+ {module: CC_FILE, method: 'removePreviewContact', trackingId: error.details.trackingId}
2449
+ );
2450
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'removePreviewContact', CC_FILE);
2451
+ });
2452
+ });
2323
2453
  });
@@ -263,7 +263,7 @@ describe('AgentConfigService', () => {
263
263
 
264
264
  expect(mockWebexRequest.request).toHaveBeenCalledWith({
265
265
  service: mockWccAPIURL,
266
- resource: `organization/${mockOrgId}/v2/auxiliary-code?page=${page}&pageSize=${pageSize}&filter=id=in=(${filter})&attributes=${attributes}`,
266
+ resource: `organization/${mockOrgId}/v2/auxiliary-code?page=${page}&pageSize=${pageSize}&filter=id=in=(${filter})&attributes=${attributes}&desktopProfileFilter=true`,
267
267
  method: 'GET',
268
268
  });
269
269
  expect(result).toEqual(mockResponse.body);
@@ -84,7 +84,12 @@ describe('TaskManager', () => {
84
84
  onSpy = jest.spyOn(webCallingService, 'on');
85
85
  offSpy = jest.spyOn(webCallingService, 'off');
86
86
 
87
- taskManager = new TaskManager(mockApiAIAssistant, contactMock, webCallingService, webSocketManagerMock);
87
+ taskManager = new TaskManager(
88
+ mockApiAIAssistant,
89
+ contactMock,
90
+ webCallingService,
91
+ webSocketManagerMock
92
+ );
88
93
  const taskMock = {
89
94
  emit: jest.fn(),
90
95
  accept: jest.fn(),
@@ -1146,7 +1151,7 @@ describe('TaskManager', () => {
1146
1151
  taskManager.setAgentId(agentId);
1147
1152
  });
1148
1153
 
1149
- it('should set wrapUpRequired to true when agent is in agentsPendingWrapUp array', () => {
1154
+ it('should set wrapUpRequired to true when agent is in agentsPendingWrapUp array for non-campaign tasks', () => {
1150
1155
  const task = taskManager.getTask(taskId);
1151
1156
  task.updateTaskData = jest.fn().mockImplementation((newData) => {
1152
1157
  task.data = {
@@ -1306,7 +1311,7 @@ describe('TaskManager', () => {
1306
1311
  );
1307
1312
  });
1308
1313
 
1309
- it('should set wrapUpRequired correctly when agent is the only one in agentsPendingWrapUp', () => {
1314
+ it('should set wrapUpRequired to true when agent is the only one in agentsPendingWrapUp for non-campaign tasks', () => {
1310
1315
  const task = taskManager.getTask(taskId);
1311
1316
  task.updateTaskData = jest.fn().mockImplementation((newData) => {
1312
1317
  task.data = {
@@ -1338,7 +1343,7 @@ describe('TaskManager', () => {
1338
1343
  );
1339
1344
  });
1340
1345
 
1341
- it('should work correctly for different interaction states when agent is in agentsPendingWrapUp', () => {
1346
+ it('should set wrapUpRequired to true for different interaction states when agent is in agentsPendingWrapUp for non-campaign tasks', () => {
1342
1347
  const task = taskManager.getTask(taskId);
1343
1348
  task.updateTaskData = jest.fn().mockImplementation((newData) => {
1344
1349
  task.data = {
@@ -1368,7 +1373,6 @@ describe('TaskManager', () => {
1368
1373
 
1369
1374
  webSocketManagerMock.emit('message', JSON.stringify(payloadConnected));
1370
1375
 
1371
- // First call should set wrapUpRequired to true
1372
1376
  expect(task.updateTaskData).toHaveBeenNthCalledWith(
1373
1377
  1,
1374
1378
  expect.objectContaining({
@@ -1391,7 +1395,6 @@ describe('TaskManager', () => {
1391
1395
 
1392
1396
  webSocketManagerMock.emit('message', JSON.stringify(payloadHeld));
1393
1397
 
1394
- // Second call should also set wrapUpRequired to true
1395
1398
  expect(task.updateTaskData).toHaveBeenNthCalledWith(
1396
1399
  2,
1397
1400
  expect.objectContaining({
@@ -1399,6 +1402,43 @@ describe('TaskManager', () => {
1399
1402
  })
1400
1403
  );
1401
1404
  });
1405
+
1406
+ it('should set wrapUpRequired to false for campaign preview tasks even when agent is in agentsPendingWrapUp', () => {
1407
+ const task = taskManager.getTask(taskId);
1408
+ // Set up task as a campaign preview task via outboundType
1409
+ task.data.interaction = {
1410
+ ...task.data.interaction,
1411
+ outboundType: 'STANDARD_PREVIEW_CAMPAIGN',
1412
+ };
1413
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1414
+ task.data = {
1415
+ ...task.data,
1416
+ ...newData,
1417
+ };
1418
+ return task;
1419
+ });
1420
+ task.unregisterWebCallListeners = jest.fn();
1421
+
1422
+ const payload = {
1423
+ data: {
1424
+ type: CC_EVENTS.CONTACT_ENDED,
1425
+ interactionId: taskId,
1426
+ interaction: {
1427
+ state: 'connected',
1428
+ mediaType: 'telephony',
1429
+ },
1430
+ agentsPendingWrapUp: [agentId],
1431
+ },
1432
+ };
1433
+
1434
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1435
+
1436
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1437
+ expect.objectContaining({
1438
+ wrapUpRequired: false,
1439
+ })
1440
+ );
1441
+ });
1402
1442
  });
1403
1443
 
1404
1444
  it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
@@ -3294,7 +3334,7 @@ describe('TaskManager', () => {
3294
3334
  );
3295
3335
  });
3296
3336
 
3297
- it('should update task data but NOT remove task when CampaignContactUpdated is received', () => {
3337
+ it('should emit TASK_CAMPAIGN_CONTACT_UPDATED and NOT remove task when CampaignContactUpdated is received', () => {
3298
3338
  const campaignInteractionId = 'campaign-interaction-123';
3299
3339
 
3300
3340
  // First create a campaign preview task
@@ -3344,8 +3384,199 @@ describe('TaskManager', () => {
3344
3384
  // Task should still exist in collection (not removed — non-terminal event)
3345
3385
  expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
3346
3386
 
3387
+ // TASK_CAMPAIGN_CONTACT_UPDATED should have been emitted
3388
+ expect(taskEmitSpy).toHaveBeenCalledWith(
3389
+ TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED,
3390
+ expect.objectContaining({
3391
+ data: expect.objectContaining({
3392
+ interactionId: campaignInteractionId,
3393
+ }),
3394
+ })
3395
+ );
3396
+
3347
3397
  // TASK_END should NOT have been emitted
3348
3398
  expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.anything());
3349
3399
  });
3400
+
3401
+ it('should emit TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED when CampaignPreviewAcceptFailed is received', () => {
3402
+ const campaignInteractionId = 'campaign-interaction-123';
3403
+
3404
+ // First create a campaign preview task
3405
+ const reservationPayload = {
3406
+ data: {
3407
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3408
+ interactionId: campaignInteractionId,
3409
+ agentId: taskDataMock.agentId,
3410
+ orgId: taskDataMock.orgId,
3411
+ trackingId: 'campaign-tracking-456',
3412
+ interaction: {
3413
+ mediaType: 'telephony',
3414
+ callProcessingDetails: {
3415
+ campaignId: 'campaign-789',
3416
+ },
3417
+ },
3418
+ },
3419
+ };
3420
+
3421
+ webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
3422
+
3423
+ const task = taskManager['taskCollection'][campaignInteractionId];
3424
+ expect(task).toBeDefined();
3425
+
3426
+ const taskEmitSpy = jest.spyOn(task, 'emit');
3427
+
3428
+ const failPayload = {
3429
+ data: {
3430
+ type: CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED,
3431
+ interactionId: campaignInteractionId,
3432
+ campaignId: 'campaign-789',
3433
+ reason: 'INTERNAL_ERROR',
3434
+ },
3435
+ };
3436
+
3437
+ webSocketManagerMock.emit('message', JSON.stringify(failPayload));
3438
+
3439
+ expect(taskEmitSpy).toHaveBeenCalledWith(
3440
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED,
3441
+ expect.objectContaining({
3442
+ data: expect.objectContaining({interactionId: campaignInteractionId}),
3443
+ })
3444
+ );
3445
+ // Task should still exist (failure is non-terminal)
3446
+ expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
3447
+
3448
+ // Failure payload (reason, campaignId) should be merged into task.data
3449
+ expect(task.data.reason).toBe('INTERNAL_ERROR');
3450
+
3451
+ // Original reservation data must be preserved
3452
+ expect(task.data.interaction).toBeDefined();
3453
+ expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
3454
+ });
3455
+
3456
+ it('should emit TASK_CAMPAIGN_PREVIEW_SKIP_FAILED when CampaignPreviewSkipFailed is received', () => {
3457
+ const campaignInteractionId = 'campaign-interaction-123';
3458
+
3459
+ const reservationPayload = {
3460
+ data: {
3461
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3462
+ interactionId: campaignInteractionId,
3463
+ agentId: taskDataMock.agentId,
3464
+ orgId: taskDataMock.orgId,
3465
+ trackingId: 'campaign-tracking-456',
3466
+ interaction: {
3467
+ mediaType: 'telephony',
3468
+ callProcessingDetails: {
3469
+ campaignId: 'campaign-789',
3470
+ },
3471
+ },
3472
+ },
3473
+ };
3474
+
3475
+ webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
3476
+
3477
+ const task = taskManager['taskCollection'][campaignInteractionId];
3478
+ expect(task).toBeDefined();
3479
+
3480
+ const taskEmitSpy = jest.spyOn(task, 'emit');
3481
+
3482
+ const failPayload = {
3483
+ data: {
3484
+ type: CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED,
3485
+ interactionId: campaignInteractionId,
3486
+ campaignId: 'campaign-789',
3487
+ reason: 'INTERNAL_ERROR',
3488
+ },
3489
+ };
3490
+
3491
+ webSocketManagerMock.emit('message', JSON.stringify(failPayload));
3492
+
3493
+ expect(taskEmitSpy).toHaveBeenCalledWith(
3494
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_SKIP_FAILED,
3495
+ expect.objectContaining({
3496
+ data: expect.objectContaining({interactionId: campaignInteractionId}),
3497
+ })
3498
+ );
3499
+ expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
3500
+
3501
+ // Failure payload (reason) should be merged into task.data
3502
+ expect(task.data.reason).toBe('INTERNAL_ERROR');
3503
+
3504
+ // Original reservation data must be preserved
3505
+ expect(task.data.interaction).toBeDefined();
3506
+ expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
3507
+ });
3508
+
3509
+ it('should emit TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED when CampaignPreviewRemoveFailed is received', () => {
3510
+ const campaignInteractionId = 'campaign-interaction-123';
3511
+
3512
+ const reservationPayload = {
3513
+ data: {
3514
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3515
+ interactionId: campaignInteractionId,
3516
+ agentId: taskDataMock.agentId,
3517
+ orgId: taskDataMock.orgId,
3518
+ trackingId: 'campaign-tracking-456',
3519
+ interaction: {
3520
+ mediaType: 'telephony',
3521
+ callProcessingDetails: {
3522
+ campaignId: 'campaign-789',
3523
+ },
3524
+ },
3525
+ },
3526
+ };
3527
+
3528
+ webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
3529
+
3530
+ const task = taskManager['taskCollection'][campaignInteractionId];
3531
+ expect(task).toBeDefined();
3532
+
3533
+ const taskEmitSpy = jest.spyOn(task, 'emit');
3534
+
3535
+ const failPayload = {
3536
+ data: {
3537
+ type: CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED,
3538
+ interactionId: campaignInteractionId,
3539
+ campaignId: 'campaign-789',
3540
+ reason: 'INTERNAL_ERROR',
3541
+ },
3542
+ };
3543
+
3544
+ webSocketManagerMock.emit('message', JSON.stringify(failPayload));
3545
+
3546
+ expect(taskEmitSpy).toHaveBeenCalledWith(
3547
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED,
3548
+ expect.objectContaining({
3549
+ data: expect.objectContaining({interactionId: campaignInteractionId}),
3550
+ })
3551
+ );
3552
+ expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
3553
+
3554
+ // Failure payload (reason) should be merged into task.data
3555
+ expect(task.data.reason).toBe('INTERNAL_ERROR');
3556
+
3557
+ // Original reservation data must be preserved
3558
+ expect(task.data.interaction).toBeDefined();
3559
+ expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
3560
+ });
3561
+
3562
+ it('should not emit campaign preview failure events when task does not exist', () => {
3563
+ const nonExistentId = 'non-existent-interaction';
3564
+
3565
+ const failPayload = {
3566
+ data: {
3567
+ type: CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED,
3568
+ interactionId: nonExistentId,
3569
+ campaignId: 'campaign-789',
3570
+ reason: 'INTERNAL_ERROR',
3571
+ },
3572
+ };
3573
+
3574
+ // Should not throw when task is not found
3575
+ expect(() => {
3576
+ webSocketManagerMock.emit('message', JSON.stringify(failPayload));
3577
+ }).not.toThrow();
3578
+
3579
+ expect(taskManager['taskCollection'][nonExistentId]).toBeUndefined();
3580
+ });
3350
3581
  });
3351
3582
  });
@@ -239,5 +239,195 @@ describe('AQM routing dialer', () => {
239
239
  ).rejects.toThrow('Request Timeout');
240
240
  });
241
241
  });
242
+
243
+ describe('skipPreviewContact', () => {
244
+ it('should construct the correct URL with campaignId and interactionId', () => {
245
+ const dialer = aqmDialer(fakeAqm as any);
246
+ const config = dialer.skipPreviewContact({data: previewPayload}) as any;
247
+
248
+ expect(config.url).toBe(
249
+ `/v1/dialer/campaign/${previewPayload.campaignId}/preview-task/${previewPayload.interactionId}/skip`
250
+ );
251
+ });
252
+
253
+ it('should URL-encode campaignId when it contains reserved characters', () => {
254
+ const dialer = aqmDialer(fakeAqm as any);
255
+ const payloadWithSpecialChars = {
256
+ interactionId: 'interaction-456',
257
+ campaignId: 'My Campaign/Test #1',
258
+ };
259
+ const config = dialer.skipPreviewContact({data: payloadWithSpecialChars}) as any;
260
+
261
+ expect(config.url).toBe(
262
+ `/v1/dialer/campaign/${encodeURIComponent(
263
+ payloadWithSpecialChars.campaignId
264
+ )}/preview-task/${payloadWithSpecialChars.interactionId}/skip`
265
+ );
266
+ expect(config.url).toContain('My%20Campaign%2FTest%20%231');
267
+ });
268
+
269
+ it('should call the skipPreviewContact api', () => {
270
+ const fakeAqm = {
271
+ req: () =>
272
+ jest.fn().mockResolvedValue(() => {
273
+ Promise.resolve({data: 'skip preview success'});
274
+ }),
275
+ evt: jest.fn(),
276
+ };
277
+
278
+ const dialer = aqmDialer(fakeAqm as any);
279
+
280
+ dialer
281
+ .skipPreviewContact({data: previewPayload})
282
+ .then((response) => {
283
+ expect(response.data).toBe('skip preview success');
284
+ })
285
+ .catch(() => {
286
+ expect(true).toBe(true);
287
+ });
288
+
289
+ expect(dialer.skipPreviewContact).toHaveBeenCalled();
290
+ });
291
+
292
+ it('should handle network errors', () => {
293
+ const fakeAqm = {
294
+ req: () => jest.fn().mockRejectedValue(new Error('Network Error')),
295
+ evt: jest.fn(),
296
+ };
297
+
298
+ const dialer = aqmDialer(fakeAqm as any);
299
+
300
+ return expect(
301
+ dialer.skipPreviewContact({
302
+ data: previewPayload,
303
+ })
304
+ ).rejects.toThrow('Network Error');
305
+ });
306
+
307
+ it('should handle server errors', () => {
308
+ const fakeAqm = {
309
+ req: () => jest.fn().mockRejectedValue(new Error('Server Error')),
310
+ evt: jest.fn(),
311
+ };
312
+
313
+ const dialer = aqmDialer(fakeAqm as any);
314
+
315
+ return expect(
316
+ dialer.skipPreviewContact({
317
+ data: previewPayload,
318
+ })
319
+ ).rejects.toThrow('Server Error');
320
+ });
321
+
322
+ it('should handle timeout scenarios', () => {
323
+ const fakeAqm = {
324
+ req: () => jest.fn().mockRejectedValue(new Error('Request Timeout')),
325
+ evt: jest.fn(),
326
+ };
327
+
328
+ const dialer = aqmDialer(fakeAqm as any);
329
+
330
+ return expect(
331
+ dialer.skipPreviewContact({
332
+ data: previewPayload,
333
+ })
334
+ ).rejects.toThrow('Request Timeout');
335
+ });
336
+ });
337
+
338
+ describe('removePreviewContact', () => {
339
+ it('should construct the correct URL with campaignId and interactionId', () => {
340
+ const dialer = aqmDialer(fakeAqm as any);
341
+ const config = dialer.removePreviewContact({data: previewPayload}) as any;
342
+
343
+ expect(config.url).toBe(
344
+ `/v1/dialer/campaign/${previewPayload.campaignId}/preview-task/${previewPayload.interactionId}/remove`
345
+ );
346
+ });
347
+
348
+ it('should URL-encode campaignId when it contains reserved characters', () => {
349
+ const dialer = aqmDialer(fakeAqm as any);
350
+ const payloadWithSpecialChars = {
351
+ interactionId: 'interaction-456',
352
+ campaignId: 'My Campaign/Test #1',
353
+ };
354
+ const config = dialer.removePreviewContact({data: payloadWithSpecialChars}) as any;
355
+
356
+ expect(config.url).toBe(
357
+ `/v1/dialer/campaign/${encodeURIComponent(
358
+ payloadWithSpecialChars.campaignId
359
+ )}/preview-task/${payloadWithSpecialChars.interactionId}/remove`
360
+ );
361
+ expect(config.url).toContain('My%20Campaign%2FTest%20%231');
362
+ });
363
+
364
+ it('should call the removePreviewContact api', () => {
365
+ const fakeAqm = {
366
+ req: () =>
367
+ jest.fn().mockResolvedValue(() => {
368
+ Promise.resolve({data: 'remove preview success'});
369
+ }),
370
+ evt: jest.fn(),
371
+ };
372
+
373
+ const dialer = aqmDialer(fakeAqm as any);
374
+
375
+ dialer
376
+ .removePreviewContact({data: previewPayload})
377
+ .then((response) => {
378
+ expect(response.data).toBe('remove preview success');
379
+ })
380
+ .catch(() => {
381
+ expect(true).toBe(true);
382
+ });
383
+
384
+ expect(dialer.removePreviewContact).toHaveBeenCalled();
385
+ });
386
+
387
+ it('should handle network errors', () => {
388
+ const fakeAqm = {
389
+ req: () => jest.fn().mockRejectedValue(new Error('Network Error')),
390
+ evt: jest.fn(),
391
+ };
392
+
393
+ const dialer = aqmDialer(fakeAqm as any);
394
+
395
+ return expect(
396
+ dialer.removePreviewContact({
397
+ data: previewPayload,
398
+ })
399
+ ).rejects.toThrow('Network Error');
400
+ });
401
+
402
+ it('should handle server errors', () => {
403
+ const fakeAqm = {
404
+ req: () => jest.fn().mockRejectedValue(new Error('Server Error')),
405
+ evt: jest.fn(),
406
+ };
407
+
408
+ const dialer = aqmDialer(fakeAqm as any);
409
+
410
+ return expect(
411
+ dialer.removePreviewContact({
412
+ data: previewPayload,
413
+ })
414
+ ).rejects.toThrow('Server Error');
415
+ });
416
+
417
+ it('should handle timeout scenarios', () => {
418
+ const fakeAqm = {
419
+ req: () => jest.fn().mockRejectedValue(new Error('Request Timeout')),
420
+ evt: jest.fn(),
421
+ };
422
+
423
+ const dialer = aqmDialer(fakeAqm as any);
424
+
425
+ return expect(
426
+ dialer.removePreviewContact({
427
+ data: previewPayload,
428
+ })
429
+ ).rejects.toThrow('Request Timeout');
430
+ });
431
+ });
242
432
  });
243
433
  });
@@ -691,6 +691,27 @@ describe('Task', () => {
691
691
  );
692
692
  });
693
693
 
694
+ it('should hold using mediaResourceId from interaction.media after recording event wipes top-level mediaResourceId', async () => {
695
+ // Set a DIFFERENT top-level mediaResourceId so we can distinguish the two sources
696
+ const staleTopLevelId = 'stale-top-level-media-resource-id';
697
+ const correctMediaId = task.data.interaction.media[task.data.interaction.mainInteractionId].mediaResourceId;
698
+ task.data.mediaResourceId = staleTopLevelId;
699
+
700
+ // Simulate recording event wiping top-level mediaResourceId (as reconcileData does)
701
+ delete task.data.mediaResourceId;
702
+
703
+ const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
704
+ contactMock.hold.mockResolvedValue(expectedResponse);
705
+
706
+ await task.hold();
707
+
708
+ // hold() should read from interaction.media, not the (now deleted) top-level field
709
+ expect(contactMock.hold).toHaveBeenCalledWith({
710
+ interactionId: taskId,
711
+ data: {mediaResourceId: correctMediaId},
712
+ });
713
+ });
714
+
694
715
  it('should handle errors in hold method', async () => {
695
716
  const error = {details: (global as any).makeFailure('Hold Failed')};
696
717
  contactMock.hold.mockImplementation(() => {