@webex/contact-center 3.12.0-task-refactor.4 → 3.12.0-task-refactor.6

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 (35) hide show
  1. package/dist/services/task/TaskManager.js +1 -0
  2. package/dist/services/task/TaskManager.js.map +1 -1
  3. package/dist/services/task/TaskUtils.js +8 -6
  4. package/dist/services/task/TaskUtils.js.map +1 -1
  5. package/dist/services/task/state-machine/TaskStateMachine.js +77 -14
  6. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  7. package/dist/services/task/state-machine/actions.js +85 -13
  8. package/dist/services/task/state-machine/actions.js.map +1 -1
  9. package/dist/services/task/state-machine/guards.js +35 -0
  10. package/dist/services/task/state-machine/guards.js.map +1 -1
  11. package/dist/services/task/state-machine/uiControlsComputer.js +76 -10
  12. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  13. package/dist/services/task/voice/Voice.js +10 -4
  14. package/dist/services/task/voice/Voice.js.map +1 -1
  15. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +68 -8
  16. package/dist/types/services/task/state-machine/guards.d.ts +5 -0
  17. package/dist/types/services/task/voice/Voice.d.ts +18 -17
  18. package/dist/webex.js +1 -1
  19. package/package.json +1 -1
  20. package/src/services/task/TaskManager.ts +1 -1
  21. package/src/services/task/TaskUtils.ts +8 -6
  22. package/src/services/task/state-machine/TaskStateMachine.ts +101 -16
  23. package/src/services/task/state-machine/actions.ts +148 -24
  24. package/src/services/task/state-machine/guards.ts +46 -0
  25. package/src/services/task/state-machine/uiControlsComputer.ts +158 -15
  26. package/src/services/task/voice/Voice.ts +12 -5
  27. package/test/unit/spec/services/WebCallingService.ts +7 -1
  28. package/test/unit/spec/services/task/TaskManager.ts +26 -0
  29. package/test/unit/spec/services/task/TaskUtils.ts +16 -0
  30. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +573 -0
  31. package/test/unit/spec/services/task/state-machine/guards.ts +88 -0
  32. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +1023 -46
  33. package/test/unit/spec/services/task/voice/Voice.ts +44 -0
  34. package/umd/contact-center.min.js +2 -2
  35. package/umd/contact-center.min.js.map +1 -1
@@ -144,16 +144,27 @@ function computeVoiceInteractionUIControls(
144
144
  Boolean(selfAgentId) &&
145
145
  Boolean(mainCallId) &&
146
146
  Boolean(interaction?.media?.[mainCallId]?.participants?.includes(selfAgentId as string));
147
+ const consultMedia = Object.values(interaction?.media ?? {}).find(
148
+ (media: any) =>
149
+ media?.mediaResourceId === taskData?.consultMediaResourceId || media?.mType === 'consult'
150
+ ) as {participants?: string[]} | undefined;
151
+ const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
152
+ const selfInConsultCall =
153
+ Boolean(selfAgentId) && Boolean(consultMedia?.participants?.includes(selfAgentId as string));
147
154
  const conferenceActive = isConferencing || conferenceFromBackend || consultFromConference;
148
155
  // Treat consult initiator as "in conference" even if mainCall participant list lags while consulting.
149
156
  const inConference = conferenceActive && (isConferencing || selfInMainCall || consultInitiator);
150
157
 
151
158
  // Check if this is a consulted agent (must be after isConsulting is computed).
152
- const isSoleAgentOnCall = participantCount <= 1 && !isConsulting && !inConference;
159
+ const isSoleAgentOnCall =
160
+ participantCount <= 1 && selfInMainCall && !isConsulting && !inConference;
153
161
  const isConsulted =
154
162
  inConference || isSoleAgentOnCall
155
163
  ? false
156
- : getIsConsultedAgentForControls(taskData, context, isConsulting);
164
+ : getIsConsultedAgentForControls(taskData, context, isConsulting) ||
165
+ (!consultInitiator &&
166
+ (selfParticipant?.isConsulted === true ||
167
+ selfParticipant?.consultState === 'consulting'));
157
168
 
158
169
  // Active call = can perform call operations
159
170
  const isActive =
@@ -173,29 +184,111 @@ function computeVoiceInteractionUIControls(
173
184
  taskData?.consultMediaResourceId ||
174
185
  Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
175
186
  );
187
+ const selfConsultPendingOnConsultMedia =
188
+ selfParticipant?.consultState === 'consultInitiated' &&
189
+ !taskData?.isConsulted &&
190
+ hasConsultMedia;
191
+ const ownerParticipant = interaction?.owner
192
+ ? interaction.participants?.[interaction.owner]
193
+ : undefined;
194
+ const otherAgentConsultInProgress = Boolean(
195
+ interaction?.participants &&
196
+ Object.values(interaction.participants).some((participant: any) => {
197
+ if (!participant || participant.hasLeft) return false;
198
+ if (participant.id === selfAgentId) return false;
199
+ if (participant.pType !== 'AGENT') return false;
200
+
201
+ return (
202
+ participant.consultState === 'consultInitiated' ||
203
+ participant.consultState === 'consultReserved' ||
204
+ participant.consultState === 'consulting' ||
205
+ participant.currentState === 'consulting'
206
+ );
207
+ })
208
+ );
209
+ const isHydratedConferenceConsultPending =
210
+ inConference && selfConsultPendingOnConsultMedia && !consultDestinationAgentJoined;
176
211
  const hasParallelConsultLeg =
177
212
  consultOwnedBySelf &&
178
213
  !isConsulting &&
179
214
  !isConsulted &&
180
215
  (consultInProgress || consultCallHeld || hasConsultMedia);
216
+ const activeLegForConferenceConsult = consultCallHeld ? 'main' : 'consult';
217
+ const isCurrentLegActive = currentLeg === activeLegForConferenceConsult;
218
+ const isConferenceConsultTransferContext =
219
+ inConference && consultInitiator && hasConsultMedia && isConsultDestinationReady;
181
220
  const consultLegOnHold = isConsulting && consultCallHeld;
221
+ const callProcessingDetails = interaction?.callProcessingDetails as
222
+ | {conferenceHoldParticipant?: boolean | string}
223
+ | undefined;
224
+ const conferenceHoldParticipant =
225
+ callProcessingDetails?.conferenceHoldParticipant === true ||
226
+ callProcessingDetails?.conferenceHoldParticipant === 'true';
227
+ const postDeclineHeldMainLeg =
228
+ consultInitiator &&
229
+ !consultDestinationAgentJoined &&
230
+ isHeld &&
231
+ inConference &&
232
+ conferenceHoldParticipant;
233
+ const postConsultCompletedHeldMainLeg =
234
+ selfParticipant?.consultState === 'consultCompleted' && isHeld && inConference && !isConsulting;
235
+ const nonOwnerPostConsultCompletedHeldMainLeg =
236
+ isHeld &&
237
+ inConference &&
238
+ !isConsulting &&
239
+ !consultInitiator &&
240
+ Boolean(selfAgentId) &&
241
+ Boolean(interaction?.owner) &&
242
+ selfAgentId !== interaction?.owner &&
243
+ ownerParticipant?.consultState === 'consultCompleted';
244
+ const isConsultPendingBeforeJoin =
245
+ selfParticipant?.consultState === 'consultInitiated' && !consultDestinationAgentJoined;
246
+ const hideExitConferenceWhileConsultPending =
247
+ currentLeg === 'main' &&
248
+ inConference &&
249
+ isConsultPendingBeforeJoin &&
250
+ (consultFromConference ||
251
+ consultInitiator ||
252
+ taskData?.type === 'AgentConsultCreated' ||
253
+ consultInProgress ||
254
+ isConsulting);
255
+ const hideExitConferenceDuringActiveConsultFromConference =
256
+ inConference &&
257
+ consultInitiator &&
258
+ consultDestinationAgentJoined &&
259
+ (isConsulting ||
260
+ taskData?.type === 'AgentConsulting' ||
261
+ selfParticipant?.consultState === 'consulting');
262
+ const forceHeldPostConsultControls =
263
+ !hideExitConferenceWhileConsultPending &&
264
+ (postDeclineHeldMainLeg || postConsultCompletedHeldMainLeg);
265
+ const selfOnConsultLeg =
266
+ selfParticipant?.consultState === 'consulting' ||
267
+ selfParticipant?.currentState === 'consulting';
268
+ const showMainLegConferenceControlsDuringConsult =
269
+ currentLeg === 'main' && inConference && consultInProgress && !selfOnConsultLeg;
270
+ const allowHeldMainLegControlsForNonInitiator =
271
+ showMainLegConferenceControlsDuringConsult && !isHydratedConferenceConsultPending;
182
272
 
183
273
  return {
184
274
  // Accept/Decline: Voice tasks in offered state
185
- // For outdial, accept is disabled (auto-answer handles it), decline remains enabled
186
- // For Extension mode (non-WebRTC), accept shows as disabled "Ringing" button
275
+ // Desktop/WebRTC + inbound: accept enabled (agent manually accepts)
276
+ // Desktop/WebRTC + outdial: accept disabled (auto-answer handles it; Widgets show "Accept" disabled)
277
+ // Extension mode (non-WebRTC): accept disabled (Widgets show "Ringing...")
187
278
  accept:
188
279
  state === TaskState.OFFERED && !interaction?.isTerminated
189
280
  ? {isVisible: true, isEnabled: isWebrtc && !isOutdial}
190
281
  : DISABLED,
191
- decline:
192
- isWebrtc && state === TaskState.OFFERED && !interaction?.isTerminated
193
- ? VISIBLE_ENABLED
194
- : DISABLED,
282
+ decline: (() => {
283
+ if (!isWebrtc || state !== TaskState.OFFERED || interaction?.isTerminated) return DISABLED;
284
+
285
+ return isOutdial ? VISIBLE_DISABLED : VISIBLE_ENABLED;
286
+ })(),
195
287
 
196
288
  // Hold: visible in connected/held/conference, disabled in conference/consulting
197
289
  hold: (() => {
198
290
  if (!hasFullControls) return DISABLED;
291
+ if (forceHeldPostConsultControls) return VISIBLE_ENABLED;
199
292
  if (consultOwnedBySelf && (isConsulting || hasParallelConsultLeg || consultCallHeld)) {
200
293
  return DISABLED;
201
294
  }
@@ -214,6 +307,7 @@ function computeVoiceInteractionUIControls(
214
307
  mute: (() => {
215
308
  if (!isWebrtc) return DISABLED;
216
309
  if (isWrappingUp) return DISABLED;
310
+ if (currentLeg === 'consult' && !selfInConsultCall) return DISABLED;
217
311
  if (isConsulting) return VISIBLE_ENABLED;
218
312
 
219
313
  if (isConnected || isHeld || isConferencing) {
@@ -227,6 +321,9 @@ function computeVoiceInteractionUIControls(
227
321
 
228
322
  // End: varies by state; during consulting only on main leg (consult held)
229
323
  end: (() => {
324
+ if (allowHeldMainLegControlsForNonInitiator) return VISIBLE_ENABLED;
325
+ if (showMainLegConferenceControlsDuringConsult) return VISIBLE_DISABLED;
326
+ if (isHydratedConferenceConsultPending && currentLeg === 'main') return VISIBLE_DISABLED;
230
327
  if (!config.isEndTaskEnabled) return DISABLED;
231
328
  if (hasParallelConsultLeg) {
232
329
  return isConnected && isEpDnConsult ? VISIBLE_ENABLED : VISIBLE_DISABLED;
@@ -240,6 +337,7 @@ function computeVoiceInteractionUIControls(
240
337
 
241
338
  if (inConference) {
242
339
  if (isConsulted) return DISABLED;
340
+ if (forceHeldPostConsultControls) return VISIBLE_DISABLED;
243
341
 
244
342
  if (consultInProgress) return VISIBLE_DISABLED;
245
343
 
@@ -253,6 +351,11 @@ function computeVoiceInteractionUIControls(
253
351
 
254
352
  // Transfer: connected/held/conference
255
353
  transfer: (() => {
354
+ if (isHydratedConferenceConsultPending) return VISIBLE_DISABLED;
355
+ if (isConferenceConsultTransferContext && currentLeg === 'main' && isCurrentLegActive) {
356
+ return DISABLED;
357
+ }
358
+ if (inConference && isConsulting && consultInitiator) return DISABLED;
256
359
  if (hasParallelConsultLeg) {
257
360
  if (!customerPresent) return DISABLED;
258
361
  if (state === TaskState.CONNECTED) return VISIBLE_ENABLED;
@@ -280,6 +383,7 @@ function computeVoiceInteractionUIControls(
280
383
  consult: (() => {
281
384
  const isConnectedOrHeld = state === TaskState.CONNECTED || state === TaskState.HELD;
282
385
 
386
+ if (inConference && nonOwnerPostConsultCompletedHeldMainLeg) return VISIBLE_DISABLED;
283
387
  if (hasParallelConsultLeg) return DISABLED;
284
388
  if (!hasFullControls || !(isConnectedOrHeld || inConference)) {
285
389
  return DISABLED;
@@ -291,7 +395,11 @@ function computeVoiceInteractionUIControls(
291
395
  if (participantCount <= 1) return VISIBLE_DISABLED;
292
396
  // Real conference: consult enabled if conditions met
293
397
  const canFromConference =
294
- !maxParticipants && customerPresent && !consultInProgress && !isConsulting;
398
+ !maxParticipants &&
399
+ customerPresent &&
400
+ !consultInProgress &&
401
+ !otherAgentConsultInProgress &&
402
+ !isConsulting;
295
403
 
296
404
  return {isVisible: true, isEnabled: canFromConference};
297
405
  }
@@ -337,6 +445,7 @@ function computeVoiceInteractionUIControls(
337
445
  // Conference: during consulting, enabled on both legs when agent joined
338
446
  // Label changes based on leg: "Conference" on main leg, "Merge" on consult leg
339
447
  conference: (() => {
448
+ if (isHydratedConferenceConsultPending && currentLeg === 'main') return VISIBLE_DISABLED;
340
449
  if (hasParallelConsultLeg) {
341
450
  if (!customerPresent) return DISABLED;
342
451
  if (state === TaskState.CONNECTED) {
@@ -359,9 +468,15 @@ function computeVoiceInteractionUIControls(
359
468
 
360
469
  // ExitConference: in conference with multiple agents in main call
361
470
  exitConference: (() => {
471
+ if (hideExitConferenceWhileConsultPending) return DISABLED;
472
+ if (allowHeldMainLegControlsForNonInitiator) return VISIBLE_ENABLED;
473
+ if (showMainLegConferenceControlsDuringConsult) return VISIBLE_DISABLED;
474
+ if (hideExitConferenceDuringActiveConsultFromConference) return DISABLED;
475
+ if (forceHeldPostConsultControls) return VISIBLE_DISABLED;
362
476
  if (isConsulted && !isConferencing) return DISABLED;
363
477
  if (!inConference) return DISABLED;
364
478
  if (participantCount <= 1) return DISABLED;
479
+ if (consultInProgress) return VISIBLE_DISABLED;
365
480
  const consultingFromConference = consultInitiator && isConsulting && conferenceFromBackend;
366
481
 
367
482
  return consultingFromConference ? VISIBLE_DISABLED : VISIBLE_ENABLED;
@@ -369,8 +484,19 @@ function computeVoiceInteractionUIControls(
369
484
 
370
485
  // TransferConference: in conference with active consult, owner consulting from conference
371
486
  transferConference: (() => {
372
- if (hasParallelConsultLeg || consultLegOnHold) return DISABLED;
373
- if (!inConference || !isConsulting) return DISABLED;
487
+ if (isConferenceConsultTransferContext && !isCurrentLegActive) return VISIBLE_DISABLED;
488
+ const consultLegTransferAvailable =
489
+ currentLeg === 'consult' && inConference && consultInitiator && hasConsultMedia;
490
+ const selfConsultingOnParticipantState = selfParticipant?.consultState === 'consulting';
491
+ const conferenceTransferAvailable =
492
+ consultLegTransferAvailable ||
493
+ (inConference &&
494
+ consultInitiator &&
495
+ hasConsultMedia &&
496
+ (isConsulting || consultInProgress || selfConsultingOnParticipantState));
497
+ if (consultLegOnHold) return DISABLED;
498
+ if (hasParallelConsultLeg && !conferenceTransferAvailable) return DISABLED;
499
+ if (!conferenceTransferAvailable && (!inConference || !isConsulting)) return DISABLED;
374
500
  if (!consultInitiator || isConsulted) return DISABLED;
375
501
 
376
502
  return isConsultDestinationReady ? VISIBLE_ENABLED : VISIBLE_DISABLED;
@@ -378,6 +504,7 @@ function computeVoiceInteractionUIControls(
378
504
 
379
505
  // MergeToConference: mirrors conference control, enabled on both legs
380
506
  mergeToConference: (() => {
507
+ if (isHydratedConferenceConsultPending && currentLeg === 'consult') return VISIBLE_DISABLED;
381
508
  if (!isConsulting || !consultInitiator) return DISABLED;
382
509
  if (!customerPresent) return VISIBLE_DISABLED;
383
510
  if (consultLegOnHold) return VISIBLE_DISABLED;
@@ -387,6 +514,7 @@ function computeVoiceInteractionUIControls(
387
514
 
388
515
  // Switch: visible only on the currently active leg
389
516
  switch: (() => {
517
+ if (isHydratedConferenceConsultPending && currentLeg === 'consult') return VISIBLE_DISABLED;
390
518
  if (!customerPresent && hasParallelConsultLeg) return DISABLED;
391
519
  if (currentLeg === 'consult') {
392
520
  if (!isConsulting || !consultInitiator || consultCallHeld) return DISABLED;
@@ -470,10 +598,18 @@ function getVoiceLegState(
470
598
  taskData?.consultMediaResourceId ||
471
599
  Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
472
600
  );
601
+ const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
602
+ const selfConsultPendingOnConsultMedia =
603
+ selfParticipant?.consultState === 'consultInitiated' &&
604
+ !taskData?.isConsulted &&
605
+ hasConsultMedia;
606
+ const selfConsultingOnConsultMedia =
607
+ selfParticipant?.consultState === 'consulting' && hasConsultMedia;
473
608
  const hasConsultLeg = Boolean(
474
- consultOwnedBySelf &&
475
- !taskData?.isConsulted &&
476
- !interaction?.isTerminated &&
609
+ !interaction?.isTerminated &&
610
+ ((consultOwnedBySelf && !taskData?.isConsulted) ||
611
+ selfConsultingOnConsultMedia ||
612
+ selfConsultPendingOnConsultMedia) &&
477
613
  (consultInProgress || isConsultingState || context.consultCallHeld || hasConsultMedia)
478
614
  );
479
615
 
@@ -486,10 +622,17 @@ function getVoiceLegState(
486
622
  };
487
623
  }
488
624
 
625
+ let mainState = TaskState.HELD;
626
+ if (currentState === TaskState.CONFERENCING) {
627
+ mainState = TaskState.CONFERENCING;
628
+ } else if (context.consultCallHeld) {
629
+ mainState = TaskState.CONNECTED;
630
+ }
631
+
489
632
  return {
490
633
  hasConsultLeg: true,
491
634
  activeLeg: context.consultCallHeld ? 'main' : 'consult',
492
- mainState: context.consultCallHeld ? TaskState.CONNECTED : TaskState.HELD,
635
+ mainState,
493
636
  consultState: isConsultingState ? currentState : TaskState.CONSULTING,
494
637
  };
495
638
  }
@@ -25,7 +25,7 @@ import Task from '../Task';
25
25
  import LoggerProxy from '../../../logger-proxy';
26
26
  import MetricsManager from '../../../metrics/MetricsManager';
27
27
  import {METRIC_EVENT_NAMES} from '../../../metrics/constants';
28
- import {TaskState, TaskEvent} from '../state-machine';
28
+ import {TaskState, TaskEvent, TaskActionArgs} from '../state-machine';
29
29
  import {WrapupData} from '../../config/types';
30
30
  import {getConsultMediaResourceId, getIsConferenceInProgress} from '../TaskUtils';
31
31
 
@@ -150,7 +150,10 @@ export default class Voice extends Task implements IVoice {
150
150
  });
151
151
  throw error;
152
152
  }
153
- } else if (!state.matches(TaskState.HELD)) {
153
+ } else if (
154
+ !state.matches(TaskState.HELD) &&
155
+ !(state.matches(TaskState.CONFERENCING) && mediaHoldState === true)
156
+ ) {
154
157
  const error = new Error(`Cannot resume call in current state: ${currentState}`);
155
158
  LoggerProxy.error('Resume operation not allowed', {
156
159
  module: CC_FILE,
@@ -1252,9 +1255,13 @@ export default class Voice extends Task implements IVoice {
1252
1255
  TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED,
1253
1256
  {updateTaskData: true}
1254
1257
  ),
1255
- emitTaskOutdialFailed: this.createEmitSelfAction(TASK_EVENTS.TASK_OUTDIAL_FAILED, {
1256
- updateTaskData: true,
1257
- }),
1258
+ emitTaskOutdialFailed: ({event}: TaskActionArgs) => {
1259
+ if (event && 'taskData' in event && event.taskData) {
1260
+ this.updateTaskData(event.taskData as TaskData);
1261
+ }
1262
+ const reason = (event as {reason?: string})?.reason || 'Outdial failed';
1263
+ this.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, reason);
1264
+ },
1258
1265
  };
1259
1266
  }
1260
1267
  }
@@ -200,11 +200,17 @@ describe('WebCallingService', () => {
200
200
 
201
201
  it('should reject if registration times out', async () => {
202
202
  line = callingClient.getLines().line1 as ILine;
203
+ jest.spyOn(global, 'setTimeout').mockImplementation(((handler: TimerHandler) => {
204
+ if (typeof handler === 'function') {
205
+ handler();
206
+ }
207
+ return 0 as unknown as NodeJS.Timeout;
208
+ }) as typeof setTimeout);
203
209
 
204
210
  const promise = webRTCCalling.registerWebCallingLine();
205
211
 
206
212
  await expect(promise).rejects.toThrow('WebCallingService Registration timed out');
207
- }, 20003); // Increased timeout to 20 seconds
213
+ });
208
214
 
209
215
  it('should handle incoming calls', async () => {
210
216
  line = callingClient.getLines().line1 as ILine;
@@ -1280,6 +1280,32 @@ describe('TaskManager', () => {
1280
1280
  sendStateMachineEventSpy.mockRestore();
1281
1281
  });
1282
1282
 
1283
+ it('should pass taskData in OUTBOUND_FAILED event for shouldWrapUp guard evaluation', () => {
1284
+ const task = taskManager.getTask(taskId);
1285
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1286
+ const payload = {
1287
+ data: {
1288
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
1289
+ interactionId: taskId,
1290
+ reason: 'CUSTOMER_BUSY',
1291
+ agentsPendingWrapUp: ['agent-123'],
1292
+ interaction: {
1293
+ outboundType: 'OUTDIAL',
1294
+ isTerminated: true,
1295
+ },
1296
+ },
1297
+ };
1298
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1299
+ const stateMachineEvent = expectLastStateMachineEvent(
1300
+ sendStateMachineEventSpy,
1301
+ TaskEvent.OUTBOUND_FAILED
1302
+ );
1303
+ expect(stateMachineEvent?.taskData).toBeDefined();
1304
+ expect(stateMachineEvent?.taskData?.agentsPendingWrapUp).toEqual(['agent-123']);
1305
+ expect(stateMachineEvent?.taskData?.interaction?.outboundType).toBe('OUTDIAL');
1306
+ sendStateMachineEventSpy.mockRestore();
1307
+ });
1308
+
1283
1309
  it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
1284
1310
  const payload = {
1285
1311
  data: {
@@ -450,6 +450,14 @@ describe('TaskUtils', () => {
450
450
  expect(getIsCustomerInCall(interaction, interactionId)).toBe(true);
451
451
  });
452
452
 
453
+ it('getIsCustomerInCall returns false when participants map is missing', () => {
454
+ const interaction = createInteraction(
455
+ {[interactionId]: {mType: 'mainCall', participants: ['c1']}},
456
+ undefined
457
+ );
458
+ expect(getIsCustomerInCall(interaction, interactionId)).toBe(false);
459
+ });
460
+
453
461
  it('getConferenceParticipantsCount counts active agents only', () => {
454
462
  const interaction = createInteraction(
455
463
  {[interactionId]: {mType: 'mainCall', participants: ['a1', 'a2', 'c1']}},
@@ -458,6 +466,14 @@ describe('TaskUtils', () => {
458
466
  expect(getConferenceParticipantsCount(interaction, interactionId)).toBe(2);
459
467
  });
460
468
 
469
+ it('getConferenceParticipantsCount returns 0 when participants map is missing', () => {
470
+ const interaction = createInteraction(
471
+ {[interactionId]: {mType: 'mainCall', participants: ['a1', 'a2', 'c1']}},
472
+ undefined
473
+ );
474
+ expect(getConferenceParticipantsCount(interaction, interactionId)).toBe(0);
475
+ });
476
+
461
477
  it('isSecondaryAgent returns true for consult with parentInteractionId', () => {
462
478
  const interaction = createInteraction();
463
479
  interaction.callProcessingDetails = {relationshipType: 'consult', parentInteractionId: 'parent-456'};