@webex/contact-center 3.12.0-task-refactor.7 → 3.12.0-task-refactor.8

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 (26) hide show
  1. package/dist/services/task/Task.js +32 -0
  2. package/dist/services/task/Task.js.map +1 -1
  3. package/dist/services/task/TaskUtils.js +3 -1
  4. package/dist/services/task/TaskUtils.js.map +1 -1
  5. package/dist/services/task/state-machine/TaskStateMachine.js +76 -0
  6. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  7. package/dist/services/task/state-machine/actions.js +113 -23
  8. package/dist/services/task/state-machine/actions.js.map +1 -1
  9. package/dist/services/task/state-machine/uiControlsComputer.js +99 -21
  10. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  11. package/dist/types/services/task/Task.d.ts +10 -0
  12. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +110 -0
  13. package/dist/types/services/task/state-machine/actions.d.ts +2 -0
  14. package/dist/webex.js +1 -1
  15. package/package.json +1 -1
  16. package/src/services/task/Task.ts +34 -0
  17. package/src/services/task/TaskUtils.ts +5 -3
  18. package/src/services/task/state-machine/TaskStateMachine.ts +104 -0
  19. package/src/services/task/state-machine/actions.ts +151 -25
  20. package/src/services/task/state-machine/uiControlsComputer.ts +173 -30
  21. package/test/unit/spec/services/task/Task.ts +61 -0
  22. package/test/unit/spec/services/task/TaskUtils.ts +65 -0
  23. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +676 -0
  24. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +597 -0
  25. package/umd/contact-center.min.js +2 -2
  26. package/umd/contact-center.min.js.map +1 -1
@@ -66,6 +66,20 @@ export function getDefaultUIControls(): TaskUIControls {
66
66
  );
67
67
  }
68
68
 
69
+ /** Consult media must exist on the interaction payload, not only as a stale resource id. */
70
+ function hasConsultMediaInInteraction(
71
+ interaction: TaskData['interaction'] | undefined,
72
+ consultMediaResourceId?: string
73
+ ): boolean {
74
+ const media = interaction?.media ?? {};
75
+ const hasConsultLeg = Object.values(media).some((entry: any) => entry?.mType === 'consult');
76
+
77
+ if (hasConsultLeg) return true;
78
+ if (!consultMediaResourceId) return false;
79
+
80
+ return Boolean(media[consultMediaResourceId]);
81
+ }
82
+
69
83
  function computeVoiceInteractionUIControls(
70
84
  state: TaskState,
71
85
  context: TaskContext,
@@ -78,8 +92,8 @@ function computeVoiceInteractionUIControls(
78
92
  return getDefaultInteractionUIControls();
79
93
  }
80
94
 
81
- // Essential data
82
- const taskData = context.taskData ?? fallbackTaskData ?? null;
95
+ // Prefer live task.data (fallback) over stale state-machine snapshot during multi-login sync.
96
+ const taskData = fallbackTaskData ?? context.taskData ?? null;
83
97
  const interaction = taskData?.interaction;
84
98
  const mainCallId = interaction?.mainInteractionId || taskData?.interactionId;
85
99
  const isWebrtc = config.voiceVariant === VOICE_VARIANT.WEBRTC;
@@ -125,7 +139,6 @@ function computeVoiceInteractionUIControls(
125
139
  // Backend sends destinationType as 'EP-DN'; SDK method uses 'entryPoint' — check both.
126
140
  const isEpDnConsult =
127
141
  consultDestinationType === 'entryPoint' || consultDestinationType === ('EP-DN' as any);
128
- const isConsultDestinationReady = consultDestinationAgentJoined || isEpDnConsult;
129
142
 
130
143
  const stateImpliesHeld = state === TaskState.HELD || state === TaskState.RESUME_INITIATING;
131
144
  const stateImpliesConnected =
@@ -151,9 +164,32 @@ function computeVoiceInteractionUIControls(
151
164
  const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
152
165
  const selfInConsultCall =
153
166
  Boolean(selfAgentId) && Boolean(consultMedia?.participants?.includes(selfAgentId as string));
154
- const conferenceActive = isConferencing || conferenceFromBackend || consultFromConference;
167
+ const hasConsultMedia = hasConsultMediaInInteraction(
168
+ interaction,
169
+ taskData?.consultMediaResourceId
170
+ );
171
+ const isConsultEndedForSelf =
172
+ taskData?.type === 'AgentConsultEnded' ||
173
+ taskData?.type === 'AgentConsultFailed' ||
174
+ (selfParticipant?.consultState === 'consultCompleted' &&
175
+ !hasConsultMedia &&
176
+ taskData?.isConsulted === false);
177
+ const effectiveConsultInitiator = isConsultEndedForSelf ? false : consultInitiator;
178
+ const effectiveConsultCallHeld = isConsultEndedForSelf ? false : consultCallHeld;
179
+ const effectiveConsultFromConference = isConsultEndedForSelf ? false : consultFromConference;
180
+ const effectiveConsultDestinationAgentJoined = isConsultEndedForSelf
181
+ ? false
182
+ : consultDestinationAgentJoined;
183
+ // After a consult ends for self, the backend stops sending consult media but the merged
184
+ // task.data can still carry the stale consult-media entry (reconcileData never deletes keys).
185
+ // Treat it as gone so post-consult main-leg controls (consult retry) are not blocked.
186
+ const effectiveHasConsultMedia = isConsultEndedForSelf ? false : hasConsultMedia;
187
+ const isConsultDestinationReady = effectiveConsultDestinationAgentJoined || isEpDnConsult;
188
+ const conferenceActive =
189
+ isConferencing || conferenceFromBackend || effectiveConsultFromConference;
155
190
  // Treat consult initiator as "in conference" even if mainCall participant list lags while consulting.
156
- const inConference = conferenceActive && (isConferencing || selfInMainCall || consultInitiator);
191
+ const inConference =
192
+ conferenceActive && (isConferencing || selfInMainCall || effectiveConsultInitiator);
157
193
 
158
194
  // Check if this is a consulted agent (must be after isConsulting is computed).
159
195
  const isSoleAgentOnCall =
@@ -162,7 +198,7 @@ function computeVoiceInteractionUIControls(
162
198
  inConference || isSoleAgentOnCall
163
199
  ? false
164
200
  : getIsConsultedAgentForControls(taskData, context, isConsulting) ||
165
- (!consultInitiator &&
201
+ (!effectiveConsultInitiator &&
166
202
  (selfParticipant?.isConsulted === true ||
167
203
  selfParticipant?.consultState === 'consulting'));
168
204
 
@@ -177,13 +213,10 @@ function computeVoiceInteractionUIControls(
177
213
 
178
214
  // Consulted agents have limited controls until they're in conference or wrapup
179
215
  // Use inConference (not isConferencing) so controls remain enabled after state downgrade
180
- const hasFullControls = !isConsulted || consultInitiator || inConference || isWrappingUp;
216
+ const hasFullControls = !isConsulted || effectiveConsultInitiator || inConference || isWrappingUp;
181
217
  const consultOwnedBySelf =
182
- consultInitiator || (Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
183
- const hasConsultMedia = Boolean(
184
- taskData?.consultMediaResourceId ||
185
- Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
186
- );
218
+ effectiveConsultInitiator ||
219
+ (Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
187
220
  const selfConsultPendingOnConsultMedia =
188
221
  selfParticipant?.consultState === 'consultInitiated' &&
189
222
  !taskData?.isConsulted &&
@@ -207,17 +240,18 @@ function computeVoiceInteractionUIControls(
207
240
  })
208
241
  );
209
242
  const isHydratedConferenceConsultPending =
210
- inConference && selfConsultPendingOnConsultMedia && !consultDestinationAgentJoined;
243
+ inConference && selfConsultPendingOnConsultMedia && !effectiveConsultDestinationAgentJoined;
211
244
  const hasParallelConsultLeg =
245
+ !isConsultEndedForSelf &&
212
246
  consultOwnedBySelf &&
213
247
  !isConsulting &&
214
248
  !isConsulted &&
215
- (consultInProgress || consultCallHeld || hasConsultMedia);
216
- const activeLegForConferenceConsult = consultCallHeld ? 'main' : 'consult';
249
+ (consultInProgress || effectiveConsultCallHeld || hasConsultMedia);
250
+ const activeLegForConferenceConsult = effectiveConsultCallHeld ? 'main' : 'consult';
217
251
  const isCurrentLegActive = currentLeg === activeLegForConferenceConsult;
218
252
  const isConferenceConsultTransferContext =
219
- inConference && consultInitiator && hasConsultMedia && isConsultDestinationReady;
220
- const consultLegOnHold = isConsulting && consultCallHeld;
253
+ inConference && effectiveConsultInitiator && hasConsultMedia && isConsultDestinationReady;
254
+ const consultLegOnHold = isConsulting && effectiveConsultCallHeld;
221
255
  const callProcessingDetails = interaction?.callProcessingDetails as
222
256
  | {conferenceHoldParticipant?: boolean | string}
223
257
  | undefined;
@@ -280,6 +314,61 @@ function computeVoiceInteractionUIControls(
280
314
  currentLeg === 'main' && inConference && consultInProgress && !selfOnConsultLeg;
281
315
  const allowHeldMainLegControlsForNonInitiator =
282
316
  showMainLegConferenceControlsDuringConsult && !isHydratedConferenceConsultPending;
317
+ const isConsultRequestedPhase =
318
+ isConsultPendingBeforeJoin &&
319
+ !isConsulted &&
320
+ (consultInitiator ||
321
+ (Boolean(selfAgentId) &&
322
+ selfParticipant?.consultState === 'consultInitiated' &&
323
+ taskData?.agentId === selfAgentId));
324
+ const isConsultUnansweredFailure =
325
+ currentLeg === 'main' &&
326
+ !inConference &&
327
+ !taskData?.isConsulted &&
328
+ !effectiveHasConsultMedia &&
329
+ isHeld &&
330
+ selfParticipant?.consultState === 'consultCompleted' &&
331
+ (taskData?.type === 'AgentConsultFailed' ||
332
+ taskData?.type === 'AgentConsultEnded' ||
333
+ isConsultEndedForSelf);
334
+
335
+ if (isConsultUnansweredFailure) {
336
+ const recordingControl =
337
+ recordingControlsAvailable && config.isRecordingEnabled && !inConference
338
+ ? VISIBLE_ENABLED
339
+ : DISABLED;
340
+
341
+ return {
342
+ ...getDefaultInteractionUIControls(),
343
+ hold: VISIBLE_ENABLED,
344
+ transfer: VISIBLE_ENABLED,
345
+ consult: VISIBLE_ENABLED,
346
+ recording: recordingControl,
347
+ end: VISIBLE_DISABLED,
348
+ };
349
+ }
350
+
351
+ if (isConsultRequestedPhase) {
352
+ if (currentLeg === 'main') {
353
+ return {
354
+ ...getDefaultInteractionUIControls(),
355
+ transfer: VISIBLE_DISABLED,
356
+ conference: VISIBLE_DISABLED,
357
+ end: VISIBLE_DISABLED,
358
+ };
359
+ }
360
+
361
+ if (currentLeg === 'consult') {
362
+ return {
363
+ ...getDefaultInteractionUIControls(),
364
+ endConsult: VISIBLE_ENABLED,
365
+ switch: VISIBLE_DISABLED,
366
+ transfer: VISIBLE_DISABLED,
367
+ transferConference: inConference ? VISIBLE_DISABLED : DISABLED,
368
+ mergeToConference: VISIBLE_DISABLED,
369
+ };
370
+ }
371
+ }
283
372
 
284
373
  return {
285
374
  // Accept/Decline: Voice tasks in offered state
@@ -300,7 +389,10 @@ function computeVoiceInteractionUIControls(
300
389
  hold: (() => {
301
390
  if (!hasFullControls) return DISABLED;
302
391
  if (forceHeldPostConsultControls) return VISIBLE_ENABLED;
303
- if (consultOwnedBySelf && (isConsulting || hasParallelConsultLeg || consultCallHeld)) {
392
+ if (
393
+ consultOwnedBySelf &&
394
+ (isConsulting || hasParallelConsultLeg || effectiveConsultCallHeld)
395
+ ) {
304
396
  return DISABLED;
305
397
  }
306
398
  if (hasParallelConsultLeg) return DISABLED;
@@ -319,7 +411,17 @@ function computeVoiceInteractionUIControls(
319
411
  if (!isWebrtc) return DISABLED;
320
412
  if (isWrappingUp) return DISABLED;
321
413
  if (currentLeg === 'consult' && !selfInConsultCall) return DISABLED;
322
- if ((isConsulting || hasParallelConsultLeg) && !isCurrentLegActive) return VISIBLE_DISABLED;
414
+ // The consulted agent has no separate consult leg; their main leg is the active call, so
415
+ // mute stays available while they consult (the heuristic below targets the initiator's
416
+ // inactive leg, not the consultee's only leg).
417
+ const isConsultedActiveMainLeg = isConsulted && currentLeg === 'main';
418
+ if (
419
+ (isConsulting || hasParallelConsultLeg) &&
420
+ !isCurrentLegActive &&
421
+ !isConsultedActiveMainLeg
422
+ ) {
423
+ return VISIBLE_DISABLED;
424
+ }
323
425
  if (isConsulting) return VISIBLE_ENABLED;
324
426
 
325
427
  if (isConnected || isHeld || isConferencing) {
@@ -395,6 +497,18 @@ function computeVoiceInteractionUIControls(
395
497
  consult: (() => {
396
498
  const isConnectedOrHeld = state === TaskState.CONNECTED || state === TaskState.HELD;
397
499
 
500
+ if (
501
+ isHeld &&
502
+ !inConference &&
503
+ !effectiveHasConsultMedia &&
504
+ selfParticipant?.consultState === 'consultCompleted' &&
505
+ !taskData?.isConsulted
506
+ ) {
507
+ const canRetryConsult = !maxParticipants && customerPresent && !otherAgentConsultInProgress;
508
+
509
+ return {isVisible: true, isEnabled: canRetryConsult};
510
+ }
511
+
398
512
  if (inConference && nonOwnerPostConsultCompletedHeldMainLeg) return VISIBLE_DISABLED;
399
513
  if (hasParallelConsultLeg) return DISABLED;
400
514
  if (!hasFullControls || !(isConnectedOrHeld || inConference)) {
@@ -591,10 +705,21 @@ function getVoiceLegState(
591
705
  };
592
706
  }
593
707
 
594
- const taskData = context.taskData ?? fallbackTaskData ?? null;
708
+ const taskData = fallbackTaskData ?? context.taskData ?? null;
595
709
  const interaction = taskData?.interaction;
596
710
  const mainCallId = interaction?.mainInteractionId || taskData?.interactionId;
597
711
  const selfAgentId = config.agentId ?? taskData?.agentId;
712
+ const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
713
+ const hasConsultMedia = hasConsultMediaInInteraction(
714
+ interaction,
715
+ taskData?.consultMediaResourceId
716
+ );
717
+ const isConsultEndedForSelf =
718
+ taskData?.type === 'AgentConsultEnded' ||
719
+ taskData?.type === 'AgentConsultFailed' ||
720
+ (selfParticipant?.consultState === 'consultCompleted' &&
721
+ !hasConsultMedia &&
722
+ taskData?.isConsulted === false);
598
723
  const consultInProgress = getIsConsultInProgressForConferenceControls(
599
724
  interaction,
600
725
  mainCallId,
@@ -607,30 +732,48 @@ function getVoiceLegState(
607
732
  const consultOwnedBySelf =
608
733
  context.consultInitiator ||
609
734
  (Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
610
- const hasConsultMedia = Boolean(
611
- taskData?.consultMediaResourceId ||
612
- Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
613
- );
614
- const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
615
735
  const selfConsultPendingOnConsultMedia =
616
736
  selfParticipant?.consultState === 'consultInitiated' &&
617
737
  !taskData?.isConsulted &&
618
738
  hasConsultMedia;
619
739
  const selfConsultingOnConsultMedia =
620
740
  selfParticipant?.consultState === 'consulting' && hasConsultMedia;
741
+ const isConsultUnansweredFailure =
742
+ !taskData?.isConsulted &&
743
+ !hasConsultMedia &&
744
+ selfParticipant?.consultState === 'consultCompleted' &&
745
+ Boolean(mainCallId && taskData?.interaction?.media?.[mainCallId]?.isHold === true) &&
746
+ taskData?.interaction?.state !== 'conference' &&
747
+ !getIsConferenceInProgress(taskData);
621
748
  const hasConsultLeg = Boolean(
622
- !interaction?.isTerminated &&
623
- ((consultOwnedBySelf && !taskData?.isConsulted) ||
624
- selfConsultingOnConsultMedia ||
625
- selfConsultPendingOnConsultMedia) &&
749
+ !isConsultEndedForSelf &&
750
+ !interaction?.isTerminated &&
751
+ !isConsultUnansweredFailure &&
752
+ (consultOwnedBySelf || selfConsultingOnConsultMedia || selfConsultPendingOnConsultMedia) &&
626
753
  (consultInProgress || isConsultingState || context.consultCallHeld || hasConsultMedia)
627
754
  );
628
755
 
629
756
  if (!hasConsultLeg) {
757
+ let mainState = currentState;
758
+ if (isConsultEndedForSelf && taskData?.interaction) {
759
+ const resolvedMainId = taskData.interaction.mainInteractionId || taskData.interactionId;
760
+ const isMainHeld = Boolean(
761
+ resolvedMainId && taskData.interaction.media?.[resolvedMainId]?.isHold === true
762
+ );
763
+
764
+ if (taskData.interaction.state === 'conference' || getIsConferenceInProgress(taskData)) {
765
+ mainState = TaskState.CONFERENCING;
766
+ } else if (isMainHeld || taskData.interaction.state === 'hold') {
767
+ mainState = TaskState.HELD;
768
+ } else {
769
+ mainState = TaskState.CONNECTED;
770
+ }
771
+ }
772
+
630
773
  return {
631
774
  hasConsultLeg: false,
632
775
  activeLeg: 'main',
633
- mainState: currentState,
776
+ mainState,
634
777
  consultState: TaskState.CONSULTING,
635
778
  };
636
779
  }
@@ -89,6 +89,67 @@ describe('Task (base class)', () => {
89
89
  expect((task.data as any).foo).toBeUndefined();
90
90
  });
91
91
 
92
+ it('drops stale interaction.media/participants entries the backend no longer sends (merge keeps other fields)', () => {
93
+ const withConsult = createTaskData({
94
+ interaction: {
95
+ callAssociatedData: {a: {value: 'keep-me'}},
96
+ media: {
97
+ main: {mediaResourceId: 'main', mType: 'mainCall', isHold: true},
98
+ consult: {mediaResourceId: 'consult', mType: 'consult', isHold: false},
99
+ },
100
+ participants: {
101
+ 'customer-1': {id: 'customer-1', pType: 'Customer'},
102
+ 'agent-1': {id: 'agent-1', pType: 'Agent'},
103
+ 'agent-2': {id: 'agent-2', pType: 'Agent', consultState: 'consulting'},
104
+ },
105
+ } as any,
106
+ }) as unknown as TaskData;
107
+ task.updateTaskData(withConsult, true);
108
+
109
+ // Backend resume snapshot: consult leg gone (only main media + main participants).
110
+ const resumeData = createTaskData({
111
+ interaction: {
112
+ media: {main: {mediaResourceId: 'main', mType: 'mainCall', isHold: false}},
113
+ participants: {
114
+ 'customer-1': {id: 'customer-1', pType: 'Customer'},
115
+ 'agent-1': {id: 'agent-1', pType: 'Agent', consultState: null},
116
+ },
117
+ } as any,
118
+ }) as unknown as TaskData;
119
+ task.updateTaskData(resumeData);
120
+
121
+ const interaction = (task.data as any).interaction;
122
+ // Stale consult media + consultee participant (absent from the resume snapshot) are removed.
123
+ expect(interaction.media.consult).toBeUndefined();
124
+ expect(interaction.participants['agent-2']).toBeUndefined();
125
+ // Entries still present in the incoming snapshot survive and reflect the new values.
126
+ expect(interaction.media.main).toBeDefined();
127
+ expect(interaction.media.main.isHold).toBe(false);
128
+ expect(interaction.participants['agent-1']).toBeDefined();
129
+ expect(interaction.participants['customer-1']).toBeDefined();
130
+ // Unrelated merge-able fields (CAD) are preserved.
131
+ expect(interaction.callAssociatedData.a.value).toBe('keep-me');
132
+ });
133
+
134
+ it('does not touch interaction maps when the incoming payload omits them', () => {
135
+ const withConsult = createTaskData({
136
+ interaction: {
137
+ media: {main: {mediaResourceId: 'main'}, consult: {mediaResourceId: 'consult'}},
138
+ participants: {'agent-1': {id: 'agent-1'}, 'agent-2': {id: 'agent-2'}},
139
+ } as any,
140
+ }) as unknown as TaskData;
141
+ task.updateTaskData(withConsult, true);
142
+
143
+ // A partial update with no interaction maps must not prune existing media/participants.
144
+ task.updateTaskData({foo: 'changed'} as unknown as TaskData);
145
+
146
+ const interaction = (task.data as any).interaction;
147
+ expect(interaction.media.main).toBeDefined();
148
+ expect(interaction.media.consult).toBeDefined();
149
+ expect(interaction.participants['agent-1']).toBeDefined();
150
+ expect(interaction.participants['agent-2']).toBeDefined();
151
+ });
152
+
92
153
  it('getUIControls returns default controls shape for idle voice task', () => {
93
154
  const controls = task.uiControls;
94
155
  const mainControls = controls.main;
@@ -13,6 +13,7 @@ import {
13
13
  isSecondaryAgent,
14
14
  isSecondaryEpDnAgent,
15
15
  getConsultMediaResourceId,
16
+ getIsConsultInProgressForConferenceControls,
16
17
  } from '../../../../../src/services/task/TaskUtils';
17
18
  import {ITask, Interaction, TaskData} from '../../../../../src/services/task/types';
18
19
  import {LoginOption} from '../../../../../src/types';
@@ -571,4 +572,68 @@ describe('TaskUtils', () => {
571
572
  expect(result).toBe('direct-id');
572
573
  });
573
574
  });
575
+
576
+ describe('getIsConsultInProgressForConferenceControls', () => {
577
+ it('returns false when consultee is reserved but has not joined (RONA)', () => {
578
+ const interaction = {
579
+ participants: {
580
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false, isConsulted: false},
581
+ 'agent-2': {
582
+ id: 'agent-2',
583
+ pType: 'Agent',
584
+ hasLeft: false,
585
+ hasJoined: false,
586
+ isConsulted: true,
587
+ consultState: 'consultReserved',
588
+ },
589
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
590
+ },
591
+ media: {
592
+ 'interaction-1': {
593
+ mType: 'mainCall',
594
+ participants: ['customer-1', 'agent-1'],
595
+ },
596
+ 'consult-media': {
597
+ mType: 'consult',
598
+ participants: ['agent-2', 'agent-1'],
599
+ },
600
+ },
601
+ } as any;
602
+
603
+ expect(
604
+ getIsConsultInProgressForConferenceControls(interaction, 'interaction-1', 'agent-1')
605
+ ).toBe(false);
606
+ });
607
+
608
+ it('returns true when consultee is actively consulting', () => {
609
+ const interaction = {
610
+ participants: {
611
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false, isConsulted: false},
612
+ 'agent-2': {
613
+ id: 'agent-2',
614
+ pType: 'Agent',
615
+ hasLeft: false,
616
+ hasJoined: true,
617
+ isConsulted: true,
618
+ consultState: 'consulting',
619
+ },
620
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
621
+ },
622
+ media: {
623
+ 'interaction-1': {
624
+ mType: 'mainCall',
625
+ participants: ['customer-1', 'agent-1'],
626
+ },
627
+ 'consult-media': {
628
+ mType: 'consult',
629
+ participants: ['agent-2', 'agent-1'],
630
+ },
631
+ },
632
+ } as any;
633
+
634
+ expect(
635
+ getIsConsultInProgressForConferenceControls(interaction, 'interaction-1', 'agent-1')
636
+ ).toBe(true);
637
+ });
638
+ });
574
639
  });