@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.
- package/dist/services/task/TaskManager.js +1 -0
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +8 -6
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +77 -14
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +85 -13
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +35 -0
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js +76 -10
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/services/task/voice/Voice.js +10 -4
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +68 -8
- package/dist/types/services/task/state-machine/guards.d.ts +5 -0
- package/dist/types/services/task/voice/Voice.d.ts +18 -17
- package/dist/webex.js +1 -1
- package/package.json +1 -1
- package/src/services/task/TaskManager.ts +1 -1
- package/src/services/task/TaskUtils.ts +8 -6
- package/src/services/task/state-machine/TaskStateMachine.ts +101 -16
- package/src/services/task/state-machine/actions.ts +148 -24
- package/src/services/task/state-machine/guards.ts +46 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +158 -15
- package/src/services/task/voice/Voice.ts +12 -5
- package/test/unit/spec/services/WebCallingService.ts +7 -1
- package/test/unit/spec/services/task/TaskManager.ts +26 -0
- package/test/unit/spec/services/task/TaskUtils.ts +16 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +573 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +88 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +1023 -46
- package/test/unit/spec/services/task/voice/Voice.ts +44 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -18,6 +18,7 @@ describe('Task state machine', () => {
|
|
|
18
18
|
const startMachine = () => {
|
|
19
19
|
const actor = createActor(createTaskStateMachine(createConfig()));
|
|
20
20
|
actor.start();
|
|
21
|
+
|
|
21
22
|
return actor;
|
|
22
23
|
};
|
|
23
24
|
|
|
@@ -124,6 +125,28 @@ describe('Task state machine', () => {
|
|
|
124
125
|
service.send({type: TaskEvent.RESUME_RECORDING});
|
|
125
126
|
expect(service.getSnapshot().context.recordingInProgress).toBe(true);
|
|
126
127
|
});
|
|
128
|
+
|
|
129
|
+
it('toggles recording state while task is held', () => {
|
|
130
|
+
const service = startMachine();
|
|
131
|
+
const taskData = createTaskData({
|
|
132
|
+
interaction: {
|
|
133
|
+
callProcessingDetails: {recordInProgress: true},
|
|
134
|
+
} as any,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
138
|
+
service.send({type: TaskEvent.ASSIGN, taskData});
|
|
139
|
+
service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
|
|
140
|
+
service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
|
|
141
|
+
expect(service.getSnapshot().value).toBe(TaskState.HELD);
|
|
142
|
+
expect(service.getSnapshot().context.recordingInProgress).toBe(true);
|
|
143
|
+
|
|
144
|
+
service.send({type: TaskEvent.PAUSE_RECORDING});
|
|
145
|
+
expect(service.getSnapshot().context.recordingInProgress).toBe(false);
|
|
146
|
+
|
|
147
|
+
service.send({type: TaskEvent.RESUME_RECORDING});
|
|
148
|
+
expect(service.getSnapshot().context.recordingInProgress).toBe(true);
|
|
149
|
+
});
|
|
127
150
|
});
|
|
128
151
|
|
|
129
152
|
describe('wrap-up and completion flow', () => {
|
|
@@ -160,6 +183,95 @@ describe('Task state machine', () => {
|
|
|
160
183
|
});
|
|
161
184
|
|
|
162
185
|
describe('consult and conference flows', () => {
|
|
186
|
+
const createSingleAgentConferenceTaskData = (interactionState: string, isHold = false) =>
|
|
187
|
+
createTaskData({
|
|
188
|
+
interactionId: 'interaction-1',
|
|
189
|
+
mediaResourceId: 'interaction-1',
|
|
190
|
+
interaction: {
|
|
191
|
+
state: interactionState,
|
|
192
|
+
mainInteractionId: 'interaction-1',
|
|
193
|
+
interactionId: 'interaction-1',
|
|
194
|
+
participants: {
|
|
195
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
|
|
196
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
197
|
+
},
|
|
198
|
+
media: {
|
|
199
|
+
'interaction-1': {
|
|
200
|
+
mediaResourceId: 'interaction-1',
|
|
201
|
+
mType: 'mainCall',
|
|
202
|
+
participants: ['agent-1', 'customer-1'],
|
|
203
|
+
isHold,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
} as any,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const createConferenceConsultTaskData = ({
|
|
210
|
+
interactionState,
|
|
211
|
+
includeSecondAgent,
|
|
212
|
+
conferenceHoldParticipant,
|
|
213
|
+
isMainHeld = false,
|
|
214
|
+
}: {
|
|
215
|
+
interactionState: string;
|
|
216
|
+
includeSecondAgent: boolean;
|
|
217
|
+
conferenceHoldParticipant: boolean | string;
|
|
218
|
+
isMainHeld?: boolean;
|
|
219
|
+
}) =>
|
|
220
|
+
createTaskData({
|
|
221
|
+
interactionId: 'interaction-1',
|
|
222
|
+
mediaResourceId: 'interaction-1',
|
|
223
|
+
consultMediaResourceId: 'consult-media-1',
|
|
224
|
+
interaction: {
|
|
225
|
+
state: interactionState,
|
|
226
|
+
mainInteractionId: 'interaction-1',
|
|
227
|
+
interactionId: 'interaction-1',
|
|
228
|
+
callProcessingDetails: {
|
|
229
|
+
conferenceHoldParticipant,
|
|
230
|
+
},
|
|
231
|
+
participants: {
|
|
232
|
+
'agent-1': {
|
|
233
|
+
id: 'agent-1',
|
|
234
|
+
pType: 'Agent',
|
|
235
|
+
hasLeft: false,
|
|
236
|
+
consultState: 'consulting',
|
|
237
|
+
},
|
|
238
|
+
...(includeSecondAgent
|
|
239
|
+
? {
|
|
240
|
+
'agent-2': {
|
|
241
|
+
id: 'agent-2',
|
|
242
|
+
pType: 'Agent',
|
|
243
|
+
hasLeft: false,
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
: {}),
|
|
247
|
+
'agent-3': {
|
|
248
|
+
id: 'agent-3',
|
|
249
|
+
pType: 'Agent',
|
|
250
|
+
hasLeft: false,
|
|
251
|
+
isConsulted: true,
|
|
252
|
+
consultState: 'consulting',
|
|
253
|
+
},
|
|
254
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
255
|
+
},
|
|
256
|
+
media: {
|
|
257
|
+
'interaction-1': {
|
|
258
|
+
mediaResourceId: 'interaction-1',
|
|
259
|
+
mType: 'mainCall',
|
|
260
|
+
participants: includeSecondAgent
|
|
261
|
+
? ['agent-1', 'agent-2', 'customer-1']
|
|
262
|
+
: ['agent-1', 'customer-1'],
|
|
263
|
+
isHold: isMainHeld,
|
|
264
|
+
},
|
|
265
|
+
'consult-media-1': {
|
|
266
|
+
mediaResourceId: 'consult-media-1',
|
|
267
|
+
mType: 'consult',
|
|
268
|
+
participants: ['agent-1', 'agent-3'],
|
|
269
|
+
isHold: false,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
} as any,
|
|
273
|
+
});
|
|
274
|
+
|
|
163
275
|
it('boots from IDLE to CONSULTING on CONSULTING_ACTIVE for split-leg ordering', () => {
|
|
164
276
|
const service = startMachine();
|
|
165
277
|
const taskData = createTaskData({
|
|
@@ -181,6 +293,154 @@ describe('Task state machine', () => {
|
|
|
181
293
|
expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(true);
|
|
182
294
|
});
|
|
183
295
|
|
|
296
|
+
it('hydrates to CONSULTING when top-level state is conference but self consultState is consulting', () => {
|
|
297
|
+
const service = startMachine();
|
|
298
|
+
const taskData = createTaskData({
|
|
299
|
+
isConsulted: false,
|
|
300
|
+
interaction: {
|
|
301
|
+
state: 'conference',
|
|
302
|
+
mainInteractionId: 'interaction-1',
|
|
303
|
+
interactionId: 'interaction-1',
|
|
304
|
+
participants: {
|
|
305
|
+
'agent-1': {
|
|
306
|
+
id: 'agent-1',
|
|
307
|
+
pType: 'Agent',
|
|
308
|
+
hasLeft: false,
|
|
309
|
+
consultState: 'consulting',
|
|
310
|
+
isConsulted: false,
|
|
311
|
+
},
|
|
312
|
+
'agent-2': {
|
|
313
|
+
id: 'agent-2',
|
|
314
|
+
pType: 'Agent',
|
|
315
|
+
hasLeft: false,
|
|
316
|
+
consultState: 'consulting',
|
|
317
|
+
isConsulted: true,
|
|
318
|
+
},
|
|
319
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
320
|
+
},
|
|
321
|
+
media: {
|
|
322
|
+
'interaction-1': {
|
|
323
|
+
mediaResourceId: 'interaction-1',
|
|
324
|
+
mType: 'mainCall',
|
|
325
|
+
participants: ['agent-1', 'customer-1'],
|
|
326
|
+
isHold: true,
|
|
327
|
+
},
|
|
328
|
+
'consult-media-1': {
|
|
329
|
+
mediaResourceId: 'consult-media-1',
|
|
330
|
+
mType: 'consult',
|
|
331
|
+
participants: ['agent-1', 'agent-2'],
|
|
332
|
+
isHold: false,
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
} as any,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
service.send({type: TaskEvent.HYDRATE, taskData});
|
|
339
|
+
|
|
340
|
+
const snapshot = service.getSnapshot();
|
|
341
|
+
expect(snapshot.value).toBe(TaskState.CONSULTING);
|
|
342
|
+
expect(snapshot.context.consultInitiator).toBe(true);
|
|
343
|
+
expect(snapshot.context.consultFromConference).toBe(true);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('hydrates consulted agent to CONSULTING when self consultState is consulting and main leg is held', () => {
|
|
347
|
+
const service = startMachine();
|
|
348
|
+
const taskData = createTaskData({
|
|
349
|
+
agentId: 'agent-2',
|
|
350
|
+
isConsulted: true,
|
|
351
|
+
interaction: {
|
|
352
|
+
state: 'conference',
|
|
353
|
+
mainInteractionId: 'interaction-1',
|
|
354
|
+
interactionId: 'interaction-1',
|
|
355
|
+
participants: {
|
|
356
|
+
'agent-1': {
|
|
357
|
+
id: 'agent-1',
|
|
358
|
+
pType: 'Agent',
|
|
359
|
+
hasLeft: false,
|
|
360
|
+
consultState: 'consulting',
|
|
361
|
+
isConsulted: false,
|
|
362
|
+
},
|
|
363
|
+
'agent-2': {
|
|
364
|
+
id: 'agent-2',
|
|
365
|
+
pType: 'Agent',
|
|
366
|
+
hasLeft: false,
|
|
367
|
+
consultState: 'consulting',
|
|
368
|
+
isConsulted: true,
|
|
369
|
+
},
|
|
370
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
371
|
+
},
|
|
372
|
+
media: {
|
|
373
|
+
'interaction-1': {
|
|
374
|
+
mediaResourceId: 'interaction-1',
|
|
375
|
+
mType: 'mainCall',
|
|
376
|
+
participants: ['agent-1', 'customer-1'],
|
|
377
|
+
isHold: true,
|
|
378
|
+
},
|
|
379
|
+
'consult-media-1': {
|
|
380
|
+
mediaResourceId: 'consult-media-1',
|
|
381
|
+
mType: 'consult',
|
|
382
|
+
participants: ['agent-1', 'agent-2'],
|
|
383
|
+
isHold: false,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
} as any,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
service.send({type: TaskEvent.HYDRATE, taskData});
|
|
390
|
+
|
|
391
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('hydrates to CONSULTING when consult is pending (self consultState is consultInitiated)', () => {
|
|
395
|
+
const service = startMachine();
|
|
396
|
+
const taskData = createTaskData({
|
|
397
|
+
isConsulted: false,
|
|
398
|
+
interaction: {
|
|
399
|
+
state: 'conference',
|
|
400
|
+
mainInteractionId: 'interaction-1',
|
|
401
|
+
interactionId: 'interaction-1',
|
|
402
|
+
participants: {
|
|
403
|
+
'agent-1': {
|
|
404
|
+
id: 'agent-1',
|
|
405
|
+
pType: 'Agent',
|
|
406
|
+
hasLeft: false,
|
|
407
|
+
consultState: 'consultInitiated',
|
|
408
|
+
isConsulted: false,
|
|
409
|
+
},
|
|
410
|
+
'agent-2': {
|
|
411
|
+
id: 'agent-2',
|
|
412
|
+
pType: 'Agent',
|
|
413
|
+
hasLeft: false,
|
|
414
|
+
consultState: 'consultReserved',
|
|
415
|
+
isConsulted: true,
|
|
416
|
+
},
|
|
417
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
418
|
+
},
|
|
419
|
+
media: {
|
|
420
|
+
'interaction-1': {
|
|
421
|
+
mediaResourceId: 'interaction-1',
|
|
422
|
+
mType: 'mainCall',
|
|
423
|
+
participants: ['agent-1', 'customer-1'],
|
|
424
|
+
isHold: true,
|
|
425
|
+
},
|
|
426
|
+
'consult-media-1': {
|
|
427
|
+
mediaResourceId: 'consult-media-1',
|
|
428
|
+
mType: 'consult',
|
|
429
|
+
participants: ['agent-1', 'agent-2'],
|
|
430
|
+
isHold: false,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
} as any,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
service.send({type: TaskEvent.HYDRATE, taskData});
|
|
437
|
+
|
|
438
|
+
const snapshot = service.getSnapshot();
|
|
439
|
+
expect(snapshot.value).toBe(TaskState.CONSULTING);
|
|
440
|
+
expect(snapshot.context.consultInitiator).toBe(true);
|
|
441
|
+
expect(snapshot.context.consultFromConference).toBe(true);
|
|
442
|
+
});
|
|
443
|
+
|
|
184
444
|
it('tracks consult destination, agent join, and clears on consult end', () => {
|
|
185
445
|
const service = startMachine();
|
|
186
446
|
const taskData = createTaskData();
|
|
@@ -211,6 +471,137 @@ describe('Task state machine', () => {
|
|
|
211
471
|
expect(snapshotAfterEnd.context.consultDestinationAgentJoined).toBe(false);
|
|
212
472
|
});
|
|
213
473
|
|
|
474
|
+
it('keeps consultDestinationAgentJoined false while consultee is only reserved, then sets true on actual join', () => {
|
|
475
|
+
const service = startMachine();
|
|
476
|
+
const pendingTaskData = createTaskData({
|
|
477
|
+
isConsulted: false,
|
|
478
|
+
interaction: {
|
|
479
|
+
state: 'conference',
|
|
480
|
+
mainInteractionId: 'interaction-1',
|
|
481
|
+
interactionId: 'interaction-1',
|
|
482
|
+
participants: {
|
|
483
|
+
'agent-1': {
|
|
484
|
+
id: 'agent-1',
|
|
485
|
+
pType: 'Agent',
|
|
486
|
+
hasLeft: false,
|
|
487
|
+
consultState: 'consultInitiated',
|
|
488
|
+
isConsulted: false,
|
|
489
|
+
},
|
|
490
|
+
'agent-2': {
|
|
491
|
+
id: 'agent-2',
|
|
492
|
+
pType: 'Agent',
|
|
493
|
+
hasLeft: false,
|
|
494
|
+
hasJoined: false,
|
|
495
|
+
consultState: 'consultReserved',
|
|
496
|
+
isConsulted: true,
|
|
497
|
+
},
|
|
498
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
499
|
+
},
|
|
500
|
+
media: {
|
|
501
|
+
'interaction-1': {
|
|
502
|
+
mediaResourceId: 'interaction-1',
|
|
503
|
+
mType: 'mainCall',
|
|
504
|
+
participants: ['agent-1', 'customer-1'],
|
|
505
|
+
isHold: true,
|
|
506
|
+
},
|
|
507
|
+
'consult-media-1': {
|
|
508
|
+
mediaResourceId: 'consult-media-1',
|
|
509
|
+
mType: 'consult',
|
|
510
|
+
participants: ['agent-1', 'agent-2'],
|
|
511
|
+
isHold: false,
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
} as any,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
service.send({type: TaskEvent.HYDRATE, taskData: pendingTaskData});
|
|
518
|
+
|
|
519
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
|
|
520
|
+
expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(false);
|
|
521
|
+
|
|
522
|
+
const joinedTaskData = {
|
|
523
|
+
...pendingTaskData,
|
|
524
|
+
interaction: {
|
|
525
|
+
...pendingTaskData.interaction,
|
|
526
|
+
participants: {
|
|
527
|
+
...pendingTaskData.interaction?.participants,
|
|
528
|
+
'agent-2': {
|
|
529
|
+
...pendingTaskData.interaction?.participants?.['agent-2'],
|
|
530
|
+
hasJoined: true,
|
|
531
|
+
consultState: 'consulting',
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
} as any;
|
|
536
|
+
|
|
537
|
+
service.send({type: TaskEvent.CONTACT_UPDATED, taskData: joinedTaskData});
|
|
538
|
+
|
|
539
|
+
expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(true);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('hydrates conference consult context flags for active consult leg controls', () => {
|
|
543
|
+
const service = startMachine();
|
|
544
|
+
const taskData = createTaskData({
|
|
545
|
+
type: 'AgentContactUnheld' as any,
|
|
546
|
+
isConsulted: false,
|
|
547
|
+
consultingAgentId: 'agent-1',
|
|
548
|
+
consultMediaResourceId: 'consult-media-1',
|
|
549
|
+
interaction: {
|
|
550
|
+
state: 'conference',
|
|
551
|
+
mainInteractionId: 'interaction-1',
|
|
552
|
+
interactionId: 'interaction-1',
|
|
553
|
+
participants: {
|
|
554
|
+
'agent-1': {
|
|
555
|
+
id: 'agent-1',
|
|
556
|
+
pType: 'Agent',
|
|
557
|
+
hasLeft: false,
|
|
558
|
+
consultState: 'consulting',
|
|
559
|
+
isConsulted: false,
|
|
560
|
+
},
|
|
561
|
+
'agent-2': {
|
|
562
|
+
id: 'agent-2',
|
|
563
|
+
pType: 'Agent',
|
|
564
|
+
hasLeft: false,
|
|
565
|
+
hasJoined: true,
|
|
566
|
+
consultState: 'consulting',
|
|
567
|
+
isConsulted: true,
|
|
568
|
+
},
|
|
569
|
+
'agent-3': {
|
|
570
|
+
id: 'agent-3',
|
|
571
|
+
pType: 'Agent',
|
|
572
|
+
hasLeft: false,
|
|
573
|
+
consultState: 'conferencing',
|
|
574
|
+
isConsulted: false,
|
|
575
|
+
},
|
|
576
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
577
|
+
},
|
|
578
|
+
media: {
|
|
579
|
+
'interaction-1': {
|
|
580
|
+
mediaResourceId: 'interaction-1',
|
|
581
|
+
mType: 'mainCall',
|
|
582
|
+
participants: ['agent-1', 'agent-3', 'customer-1'],
|
|
583
|
+
isHold: false,
|
|
584
|
+
},
|
|
585
|
+
'consult-media-1': {
|
|
586
|
+
mediaResourceId: 'consult-media-1',
|
|
587
|
+
mType: 'consult',
|
|
588
|
+
participants: ['agent-1', 'agent-2'],
|
|
589
|
+
isHold: false,
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
} as any,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
service.send({type: TaskEvent.HYDRATE, taskData});
|
|
596
|
+
|
|
597
|
+
const snapshot = service.getSnapshot();
|
|
598
|
+
expect(snapshot.value).toBe(TaskState.CONSULTING);
|
|
599
|
+
expect(snapshot.context.consultInitiator).toBe(true);
|
|
600
|
+
expect(snapshot.context.consultFromConference).toBe(true);
|
|
601
|
+
expect(snapshot.context.consultDestinationAgentJoined).toBe(true);
|
|
602
|
+
expect(snapshot.context.consultCallHeld).toBe(false);
|
|
603
|
+
});
|
|
604
|
+
|
|
214
605
|
it('returns to connected when consult ends after switching back to the main leg', () => {
|
|
215
606
|
const service = startMachine();
|
|
216
607
|
const taskData = createTaskData();
|
|
@@ -234,6 +625,89 @@ describe('Task state machine', () => {
|
|
|
234
625
|
expect(snapshotAfterEnd.context.consultCallHeld).toBe(false);
|
|
235
626
|
});
|
|
236
627
|
|
|
628
|
+
it('downgrades to HELD on CONSULT_END when conference has downgraded and conferenceHoldParticipant is true', () => {
|
|
629
|
+
const service = startMachine();
|
|
630
|
+
const conferenceTaskData = createConferenceConsultTaskData({
|
|
631
|
+
interactionState: 'conference',
|
|
632
|
+
includeSecondAgent: true,
|
|
633
|
+
conferenceHoldParticipant: false,
|
|
634
|
+
});
|
|
635
|
+
const downgradedHeldTaskData = createConferenceConsultTaskData({
|
|
636
|
+
interactionState: 'hold',
|
|
637
|
+
includeSecondAgent: false,
|
|
638
|
+
conferenceHoldParticipant: true,
|
|
639
|
+
isMainHeld: true,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
|
|
643
|
+
service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
|
|
644
|
+
service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
|
|
645
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
646
|
+
|
|
647
|
+
service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
|
|
648
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
|
|
649
|
+
service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: conferenceTaskData});
|
|
650
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
|
|
651
|
+
|
|
652
|
+
service.send({type: TaskEvent.CONSULT_END, taskData: downgradedHeldTaskData});
|
|
653
|
+
expect(service.getSnapshot().value).toBe(TaskState.HELD);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('downgrades to CONNECTED on CONSULT_END when conference has downgraded and conferenceHoldParticipant is false', () => {
|
|
657
|
+
const service = startMachine();
|
|
658
|
+
const conferenceTaskData = createConferenceConsultTaskData({
|
|
659
|
+
interactionState: 'conference',
|
|
660
|
+
includeSecondAgent: true,
|
|
661
|
+
conferenceHoldParticipant: false,
|
|
662
|
+
});
|
|
663
|
+
const downgradedConnectedTaskData = createConferenceConsultTaskData({
|
|
664
|
+
interactionState: 'connected',
|
|
665
|
+
includeSecondAgent: false,
|
|
666
|
+
conferenceHoldParticipant: false,
|
|
667
|
+
isMainHeld: false,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
|
|
671
|
+
service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
|
|
672
|
+
service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
|
|
673
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
674
|
+
|
|
675
|
+
service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
|
|
676
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
|
|
677
|
+
service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: conferenceTaskData});
|
|
678
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
|
|
679
|
+
|
|
680
|
+
service.send({type: TaskEvent.CONSULT_END, taskData: downgradedConnectedTaskData});
|
|
681
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('returns to CONFERENCING on CONSULT_END when conference is still active', () => {
|
|
685
|
+
const service = startMachine();
|
|
686
|
+
const conferenceTaskData = createConferenceConsultTaskData({
|
|
687
|
+
interactionState: 'conference',
|
|
688
|
+
includeSecondAgent: true,
|
|
689
|
+
conferenceHoldParticipant: false,
|
|
690
|
+
});
|
|
691
|
+
const stillConferenceTaskData = createConferenceConsultTaskData({
|
|
692
|
+
interactionState: 'conference',
|
|
693
|
+
includeSecondAgent: true,
|
|
694
|
+
conferenceHoldParticipant: false,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
|
|
698
|
+
service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
|
|
699
|
+
service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
|
|
700
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
701
|
+
|
|
702
|
+
service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
|
|
703
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
|
|
704
|
+
service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: conferenceTaskData});
|
|
705
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
|
|
706
|
+
|
|
707
|
+
service.send({type: TaskEvent.CONSULT_END, taskData: stillConferenceTaskData});
|
|
708
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
709
|
+
});
|
|
710
|
+
|
|
237
711
|
it('transitions to conferencing when merge event is received', () => {
|
|
238
712
|
const service = startMachine();
|
|
239
713
|
const taskData = createTaskData({consultingAgentId: 'agent-1'});
|
|
@@ -372,6 +846,44 @@ describe('Task state machine', () => {
|
|
|
372
846
|
expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
|
|
373
847
|
});
|
|
374
848
|
|
|
849
|
+
it('downgrades to HELD on HOLD_SUCCESS when no other agents remain on task', () => {
|
|
850
|
+
const service = startMachine();
|
|
851
|
+
const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
|
|
852
|
+
const heldTaskData = createSingleAgentConferenceTaskData('hold', true);
|
|
853
|
+
|
|
854
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
|
|
855
|
+
service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
|
|
856
|
+
service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
|
|
857
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
858
|
+
|
|
859
|
+
service.send({
|
|
860
|
+
type: TaskEvent.HOLD_SUCCESS,
|
|
861
|
+
mediaResourceId: heldTaskData.mediaResourceId,
|
|
862
|
+
taskData: heldTaskData,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
expect(service.getSnapshot().value).toBe(TaskState.HELD);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it('downgrades to CONNECTED on UNHOLD_SUCCESS when no other agents remain on task', () => {
|
|
869
|
+
const service = startMachine();
|
|
870
|
+
const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
|
|
871
|
+
const connectedTaskData = createSingleAgentConferenceTaskData('connected', false);
|
|
872
|
+
|
|
873
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
|
|
874
|
+
service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
|
|
875
|
+
service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
|
|
876
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
877
|
+
|
|
878
|
+
service.send({
|
|
879
|
+
type: TaskEvent.UNHOLD_SUCCESS,
|
|
880
|
+
mediaResourceId: connectedTaskData.mediaResourceId,
|
|
881
|
+
taskData: connectedTaskData,
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
|
|
885
|
+
});
|
|
886
|
+
|
|
375
887
|
it('returns to CONNECTED when CTQ cancel arrives before queue connects', () => {
|
|
376
888
|
const service = startMachine();
|
|
377
889
|
const taskData = createTaskData();
|
|
@@ -573,4 +1085,65 @@ describe('Task state machine', () => {
|
|
|
573
1085
|
expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
|
|
574
1086
|
});
|
|
575
1087
|
});
|
|
1088
|
+
|
|
1089
|
+
describe('OUTBOUND_FAILED handling', () => {
|
|
1090
|
+
it('transitions from IDLE to TERMINATED on OUTBOUND_FAILED (race condition)', () => {
|
|
1091
|
+
const service = startMachine();
|
|
1092
|
+
expect(service.getSnapshot().value).toBe(TaskState.IDLE);
|
|
1093
|
+
|
|
1094
|
+
const taskData = createTaskData({
|
|
1095
|
+
interaction: {
|
|
1096
|
+
outboundType: 'OUTDIAL',
|
|
1097
|
+
isTerminated: true,
|
|
1098
|
+
} as any,
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
service.send({type: TaskEvent.OUTBOUND_FAILED, taskData, reason: 'CUSTOMER_BUSY'});
|
|
1102
|
+
expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it('transitions from OFFERED to TERMINATED on OUTBOUND_FAILED without wrapup', () => {
|
|
1106
|
+
const service = startMachine();
|
|
1107
|
+
const offerTaskData = createTaskData({
|
|
1108
|
+
interaction: {
|
|
1109
|
+
outboundType: 'OUTDIAL',
|
|
1110
|
+
} as any,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
|
|
1114
|
+
expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
|
|
1115
|
+
|
|
1116
|
+
const failedTaskData = createTaskData({
|
|
1117
|
+
interaction: {
|
|
1118
|
+
outboundType: 'OUTDIAL',
|
|
1119
|
+
isTerminated: true,
|
|
1120
|
+
} as any,
|
|
1121
|
+
});
|
|
1122
|
+
service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
|
|
1123
|
+
expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it('transitions from OFFERED to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
|
|
1127
|
+
const service = startMachine();
|
|
1128
|
+
const offerTaskData = createTaskData({
|
|
1129
|
+
interaction: {
|
|
1130
|
+
outboundType: 'OUTDIAL',
|
|
1131
|
+
} as any,
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
|
|
1135
|
+
expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
|
|
1136
|
+
|
|
1137
|
+
const failedTaskData = createTaskData({
|
|
1138
|
+
agentId: 'agent-1',
|
|
1139
|
+
agentsPendingWrapUp: ['agent-1'],
|
|
1140
|
+
interaction: {
|
|
1141
|
+
outboundType: 'OUTDIAL',
|
|
1142
|
+
isTerminated: true,
|
|
1143
|
+
} as any,
|
|
1144
|
+
});
|
|
1145
|
+
service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
|
|
1146
|
+
expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
576
1149
|
});
|