@webex/contact-center 3.12.0-llmrefactor.2 → 3.12.0-llmrefactor.4

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 (187) hide show
  1. package/.sdd/manifest.json +5 -4
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +6 -2
  4. package/ai-docs/SECURITY.md +1 -1
  5. package/ai-docs/contact-center-spec.md +22 -4
  6. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  7. package/dist/cc.js +440 -10
  8. package/dist/cc.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +10 -2
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics/behavioral-events.js +13 -0
  15. package/dist/metrics/behavioral-events.js.map +1 -1
  16. package/dist/metrics/constants.js +2 -0
  17. package/dist/metrics/constants.js.map +1 -1
  18. package/dist/services/AddressBook.js +18 -15
  19. package/dist/services/AddressBook.js.map +1 -1
  20. package/dist/services/AnswerCallOnWebexService.js +174 -0
  21. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  22. package/dist/services/EntryPoint.js +46 -68
  23. package/dist/services/EntryPoint.js.map +1 -1
  24. package/dist/services/Queue.js +27 -22
  25. package/dist/services/Queue.js.map +1 -1
  26. package/dist/services/WebexCrossClientService.js +171 -0
  27. package/dist/services/WebexCrossClientService.js.map +1 -0
  28. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  29. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  30. package/dist/services/config/Util.js +3 -0
  31. package/dist/services/config/Util.js.map +1 -1
  32. package/dist/services/config/constants.js +10 -6
  33. package/dist/services/config/constants.js.map +1 -1
  34. package/dist/services/config/types.js +4 -0
  35. package/dist/services/config/types.js.map +1 -1
  36. package/dist/services/core/Err.js.map +1 -1
  37. package/dist/services/core/WebexRequest.js +6 -2
  38. package/dist/services/core/WebexRequest.js.map +1 -1
  39. package/dist/services/core/aqm-reqs.js +28 -14
  40. package/dist/services/core/aqm-reqs.js.map +1 -1
  41. package/dist/services/core/types.js.map +1 -1
  42. package/dist/services/task/Task.js +73 -7
  43. package/dist/services/task/Task.js.map +1 -1
  44. package/dist/services/task/TaskFactory.js +8 -4
  45. package/dist/services/task/TaskFactory.js.map +1 -1
  46. package/dist/services/task/TaskManager.js +353 -21
  47. package/dist/services/task/TaskManager.js.map +1 -1
  48. package/dist/services/task/WebexCallingUtils.js +70 -0
  49. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  50. package/dist/services/task/constants.js +4 -1
  51. package/dist/services/task/constants.js.map +1 -1
  52. package/dist/services/task/contact.js +29 -0
  53. package/dist/services/task/contact.js.map +1 -1
  54. package/dist/services/task/digital/Digital.js +3 -2
  55. package/dist/services/task/digital/Digital.js.map +1 -1
  56. package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
  57. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  58. package/dist/services/task/state-machine/actions.js +6 -1
  59. package/dist/services/task/state-machine/actions.js.map +1 -1
  60. package/dist/services/task/state-machine/guards.js +38 -11
  61. package/dist/services/task/state-machine/guards.js.map +1 -1
  62. package/dist/services/task/state-machine/types.js.map +1 -1
  63. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  64. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  65. package/dist/services/task/types.js +16 -1
  66. package/dist/services/task/types.js.map +1 -1
  67. package/dist/services/task/voice/Voice.js +350 -14
  68. package/dist/services/task/voice/Voice.js.map +1 -1
  69. package/dist/services/task/voice/WebRTC.js +4 -1
  70. package/dist/services/task/voice/WebRTC.js.map +1 -1
  71. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  72. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  73. package/dist/services/wxAppTelephonyUtils.js +19 -0
  74. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  75. package/dist/types/cc.d.ts +77 -4
  76. package/dist/types/config.d.ts +7 -0
  77. package/dist/types/constants.d.ts +8 -0
  78. package/dist/types/index.d.ts +3 -1
  79. package/dist/types/metrics/constants.d.ts +2 -0
  80. package/dist/types/services/AddressBook.d.ts +2 -1
  81. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  82. package/dist/types/services/EntryPoint.d.ts +7 -5
  83. package/dist/types/services/Queue.d.ts +5 -3
  84. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  85. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  86. package/dist/types/services/config/constants.d.ts +8 -5
  87. package/dist/types/services/config/types.d.ts +17 -4
  88. package/dist/types/services/core/Err.d.ts +2 -0
  89. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  90. package/dist/types/services/core/types.d.ts +2 -0
  91. package/dist/types/services/task/Task.d.ts +20 -2
  92. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  93. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  94. package/dist/types/services/task/constants.d.ts +3 -0
  95. package/dist/types/services/task/contact.d.ts +4 -0
  96. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  97. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
  98. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  99. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  100. package/dist/types/services/task/types.d.ts +81 -4
  101. package/dist/types/services/task/voice/Voice.d.ts +48 -9
  102. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  103. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  104. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  105. package/dist/types/types.d.ts +14 -8
  106. package/dist/types/utils/PageCache.d.ts +20 -3
  107. package/dist/types.js +7 -2
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/PageCache.js +19 -5
  110. package/dist/utils/PageCache.js.map +1 -1
  111. package/dist/webex.js +1 -1
  112. package/package.json +9 -9
  113. package/src/cc.ts +506 -9
  114. package/src/config.ts +7 -0
  115. package/src/constants.ts +8 -0
  116. package/src/index.ts +2 -0
  117. package/src/metrics/ai-docs/metrics-spec.md +9 -3
  118. package/src/metrics/behavioral-events.ts +14 -0
  119. package/src/metrics/constants.ts +2 -0
  120. package/src/services/AddressBook.ts +17 -6
  121. package/src/services/AnswerCallOnWebexService.ts +206 -0
  122. package/src/services/EntryPoint.ts +59 -60
  123. package/src/services/Queue.ts +29 -12
  124. package/src/services/WebexCrossClientService.ts +212 -0
  125. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  126. package/src/services/ai-docs/AGENTS.md +10 -10
  127. package/src/services/ai-docs/services-spec.md +6 -1
  128. package/src/services/config/Util.ts +3 -0
  129. package/src/services/config/ai-docs/AGENTS.md +1 -1
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  131. package/src/services/config/ai-docs/config-spec.md +6 -0
  132. package/src/services/config/constants.ts +10 -6
  133. package/src/services/config/types.ts +16 -4
  134. package/src/services/core/Err.ts +1 -0
  135. package/src/services/core/WebexRequest.ts +3 -1
  136. package/src/services/core/ai-docs/core-spec.md +5 -1
  137. package/src/services/core/aqm-reqs.ts +30 -15
  138. package/src/services/core/types.ts +2 -0
  139. package/src/services/task/Task.ts +79 -8
  140. package/src/services/task/TaskFactory.ts +8 -3
  141. package/src/services/task/TaskManager.ts +522 -16
  142. package/src/services/task/WebexCallingUtils.ts +136 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  144. package/src/services/task/ai-docs/task-spec.md +152 -2
  145. package/src/services/task/constants.ts +3 -0
  146. package/src/services/task/contact.ts +30 -0
  147. package/src/services/task/digital/Digital.ts +4 -2
  148. package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
  149. package/src/services/task/state-machine/actions.ts +5 -2
  150. package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
  151. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
  152. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +56 -24
  153. package/src/services/task/state-machine/guards.ts +64 -11
  154. package/src/services/task/state-machine/types.ts +16 -1
  155. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  156. package/src/services/task/types.ts +114 -4
  157. package/src/services/task/voice/Voice.ts +462 -12
  158. package/src/services/task/voice/WebRTC.ts +5 -1
  159. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  160. package/src/services/wxAppTelephonyUtils.ts +14 -0
  161. package/src/types.ts +33 -9
  162. package/src/utils/AGENTS.md +21 -10
  163. package/src/utils/PageCache.ts +38 -5
  164. package/src/utils/ai-docs/utils-spec.md +21 -11
  165. package/test/unit/spec/cc.ts +1274 -0
  166. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  167. package/test/unit/spec/services/AddressBook.ts +37 -6
  168. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  169. package/test/unit/spec/services/EntryPoint.ts +87 -40
  170. package/test/unit/spec/services/Queue.ts +123 -12
  171. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  172. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  173. package/test/unit/spec/services/config/Util.ts +85 -0
  174. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  175. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  176. package/test/unit/spec/services/task/Task.ts +200 -0
  177. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  178. package/test/unit/spec/services/task/TaskManager.ts +1294 -1
  179. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  180. package/test/unit/spec/services/task/contact.ts +33 -0
  181. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +854 -72
  182. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  183. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  184. package/test/unit/spec/services/task/voice/Voice.ts +874 -0
  185. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  186. package/umd/contact-center.min.js +2 -2
  187. package/umd/contact-center.min.js.map +1 -1
@@ -5,6 +5,7 @@ import {
5
5
  TaskState,
6
6
  } from '../../../../../../src/services/task/state-machine';
7
7
  import {createTaskData} from '../taskTestUtils';
8
+ import {guards} from '../../../../../../src/services/task/state-machine/guards';
8
9
 
9
10
  const createConfig = () => ({
10
11
  channelType: 'voice' as const,
@@ -106,6 +107,198 @@ describe('Task state machine', () => {
106
107
  });
107
108
  });
108
109
 
110
+ describe('partial lifecycle payloads', () => {
111
+ const createActiveMainCallTaskData = (isHold = false) =>
112
+ createTaskData({
113
+ agentId: 'agent-1',
114
+ interactionId: 'interaction-1',
115
+ mediaResourceId: 'main-media',
116
+ interaction: {
117
+ state: 'connected',
118
+ participants: {
119
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
120
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
121
+ },
122
+ media: {
123
+ 'main-media': {
124
+ mediaResourceId: 'main-media',
125
+ mType: 'mainCall',
126
+ participants: ['agent-1', 'customer-1'],
127
+ isHold,
128
+ },
129
+ },
130
+ } as any,
131
+ });
132
+
133
+ const createPartialMainCallTaskData = (isHold = false, omitSelf = false) =>
134
+ createTaskData({
135
+ agentId: 'agent-1',
136
+ interactionId: 'interaction-1',
137
+ mediaResourceId: 'main-media',
138
+ interaction: {
139
+ state: 'connected',
140
+ participants: omitSelf
141
+ ? {'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false}}
142
+ : {
143
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
144
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
145
+ },
146
+ media: {
147
+ 'main-media': {
148
+ mediaResourceId: 'main-media',
149
+ mType: 'mainCall',
150
+ participants: ['customer-1'],
151
+ isHold,
152
+ },
153
+ },
154
+ } as any,
155
+ });
156
+
157
+ const startMachineWithEndSpy = () => {
158
+ const emitTaskEnd = jest.fn();
159
+ const service = createActor(
160
+ createTaskStateMachine({...createConfig(), agentId: 'agent-1'}, {actions: {emitTaskEnd}})
161
+ );
162
+ service.start();
163
+
164
+ return {service, emitTaskEnd};
165
+ };
166
+
167
+ const enterTransientHoldState = (
168
+ service: ReturnType<typeof startMachine>,
169
+ state: TaskState.HOLD_INITIATING | TaskState.RESUME_INITIATING,
170
+ taskData: ReturnType<typeof createActiveMainCallTaskData>
171
+ ) => {
172
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
173
+ service.send({type: TaskEvent.ASSIGN, taskData});
174
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: 'main-media'});
175
+
176
+ if (state === TaskState.RESUME_INITIATING) {
177
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: 'main-media'});
178
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: 'main-media'});
179
+ }
180
+
181
+ expect(service.getSnapshot().value).toBe(state);
182
+ };
183
+
184
+ it('keeps CONNECTED when another participant leave event has a partial mainCall', () => {
185
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
186
+ const taskData = createActiveMainCallTaskData();
187
+
188
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
189
+ service.send({type: TaskEvent.ASSIGN, taskData});
190
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
191
+
192
+ service.send({
193
+ type: TaskEvent.PARTICIPANT_LEAVE,
194
+ participantId: 'agent-2',
195
+ taskData: createPartialMainCallTaskData(false, true),
196
+ });
197
+
198
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
199
+ expect(emitTaskEnd).not.toHaveBeenCalled();
200
+ });
201
+
202
+ it('terminates CONNECTED when an out-of-order participant leave identifies the current agent', () => {
203
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
204
+ const taskData = createActiveMainCallTaskData();
205
+
206
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
207
+ service.send({type: TaskEvent.ASSIGN, taskData});
208
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
209
+
210
+ service.send({
211
+ type: TaskEvent.PARTICIPANT_LEAVE,
212
+ participantId: 'agent-1',
213
+ taskData: createPartialMainCallTaskData(false, true),
214
+ });
215
+
216
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
217
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
218
+ });
219
+
220
+ it.each([TaskState.HOLD_INITIATING, TaskState.RESUME_INITIATING])(
221
+ 'terminates %s when a remote Drop identifies the current agent',
222
+ (state) => {
223
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
224
+ const taskData = createActiveMainCallTaskData(state === TaskState.RESUME_INITIATING);
225
+
226
+ enterTransientHoldState(service, state, taskData);
227
+ service.send({
228
+ type: TaskEvent.PARTICIPANT_LEAVE,
229
+ participantId: 'agent-1',
230
+ taskData: createPartialMainCallTaskData(
231
+ state === TaskState.RESUME_INITIATING,
232
+ true
233
+ ),
234
+ });
235
+
236
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
237
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
238
+ }
239
+ );
240
+
241
+ it.each([TaskState.HOLD_INITIATING, TaskState.RESUME_INITIATING])(
242
+ 'keeps %s when ParticipantLeftConference identifies another participant',
243
+ (state) => {
244
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
245
+ const taskData = createActiveMainCallTaskData(state === TaskState.RESUME_INITIATING);
246
+
247
+ enterTransientHoldState(service, state, taskData);
248
+ service.send({
249
+ type: TaskEvent.PARTICIPANT_LEAVE,
250
+ participantId: 'agent-2',
251
+ taskData: createPartialMainCallTaskData(
252
+ state === TaskState.RESUME_INITIATING,
253
+ true
254
+ ),
255
+ });
256
+
257
+ expect(service.getSnapshot().value).toBe(state);
258
+ expect(emitTaskEnd).not.toHaveBeenCalled();
259
+ }
260
+ );
261
+
262
+ it('keeps HELD when another participant leave event omits self from its roster', () => {
263
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
264
+ const taskData = createActiveMainCallTaskData(true);
265
+
266
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
267
+ service.send({type: TaskEvent.ASSIGN, taskData});
268
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: 'main-media'});
269
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: 'main-media'});
270
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
271
+
272
+ service.send({
273
+ type: TaskEvent.PARTICIPANT_LEAVE,
274
+ participantId: 'agent-2',
275
+ taskData: createPartialMainCallTaskData(true, true),
276
+ });
277
+
278
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
279
+ expect(emitTaskEnd).not.toHaveBeenCalled();
280
+ });
281
+
282
+ it('keeps HELD when CONSULT_END has a partial mainCall', () => {
283
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
284
+ const taskData = createActiveMainCallTaskData(true);
285
+
286
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
287
+ service.send({type: TaskEvent.ASSIGN, taskData});
288
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: 'main-media'});
289
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: 'main-media'});
290
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
291
+
292
+ service.send({
293
+ type: TaskEvent.CONSULT_END,
294
+ taskData: createPartialMainCallTaskData(true),
295
+ });
296
+
297
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
298
+ expect(emitTaskEnd).not.toHaveBeenCalled();
299
+ });
300
+ });
301
+
109
302
  describe('recording pause/resume events', () => {
110
303
  it('toggles recordingPaused flag based on events', () => {
111
304
  const service = startMachine();
@@ -180,6 +373,32 @@ describe('Task state machine', () => {
180
373
  service.send({type: TaskEvent.WRAPUP_COMPLETE});
181
374
  expect(service.getSnapshot().value).toBe(TaskState.COMPLETED);
182
375
  });
376
+
377
+ it('emits task wrappedup before cleanup on COMPLETED entry', () => {
378
+ const actionOrder: string[] = [];
379
+ const actor = createActor(
380
+ createTaskStateMachine(createConfig(), {
381
+ actions: {
382
+ emitTaskWrappedup: () => {
383
+ actionOrder.push('emitTaskWrappedup');
384
+ },
385
+ cleanupResources: () => {
386
+ actionOrder.push('cleanupResources');
387
+ },
388
+ },
389
+ })
390
+ );
391
+ actor.start();
392
+ const taskData = createTaskData();
393
+
394
+ actor.send({type: TaskEvent.TASK_INCOMING, taskData});
395
+ actor.send({type: TaskEvent.ASSIGN, taskData});
396
+ actor.send({type: TaskEvent.TASK_WRAPUP});
397
+ actor.send({type: TaskEvent.WRAPUP_COMPLETE});
398
+
399
+ expect(actor.getSnapshot().value).toBe(TaskState.COMPLETED);
400
+ expect(actionOrder).toEqual(['emitTaskWrappedup', 'cleanupResources']);
401
+ });
183
402
  });
184
403
 
185
404
  describe('consult and conference flows', () => {
@@ -343,53 +562,53 @@ describe('Task state machine', () => {
343
562
  expect(snapshot.context.consultFromConference).toBe(true);
344
563
  });
345
564
 
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,
565
+ it('hydrates consulted agent to CONSULTING when self consultState is consulting and main leg is held', () => {
566
+ const service = startMachine();
567
+ const taskData = createTaskData({
568
+ agentId: 'agent-2',
569
+ isConsulted: true,
570
+ interaction: {
571
+ state: 'conference',
572
+ mainInteractionId: 'interaction-1',
573
+ interactionId: 'interaction-1',
574
+ participants: {
575
+ 'agent-1': {
576
+ id: 'agent-1',
577
+ pType: 'Agent',
578
+ hasLeft: false,
579
+ consultState: 'consulting',
580
+ isConsulted: false,
581
+ },
582
+ 'agent-2': {
583
+ id: 'agent-2',
584
+ pType: 'Agent',
585
+ hasLeft: false,
586
+ consultState: 'consulting',
587
+ isConsulted: true,
588
+ },
589
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
384
590
  },
385
- },
386
- } as any,
387
- });
591
+ media: {
592
+ 'interaction-1': {
593
+ mediaResourceId: 'interaction-1',
594
+ mType: 'mainCall',
595
+ participants: ['agent-1', 'customer-1'],
596
+ isHold: true,
597
+ },
598
+ 'consult-media-1': {
599
+ mediaResourceId: 'consult-media-1',
600
+ mType: 'consult',
601
+ participants: ['agent-1', 'agent-2'],
602
+ isHold: false,
603
+ },
604
+ },
605
+ } as any,
606
+ });
388
607
 
389
- service.send({type: TaskEvent.HYDRATE, taskData});
608
+ service.send({type: TaskEvent.HYDRATE, taskData});
390
609
 
391
- expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
392
- });
610
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
611
+ });
393
612
 
394
613
  it('hydrates to CONSULTING when consult is pending (self consultState is consultInitiated)', () => {
395
614
  const service = startMachine();
@@ -1684,6 +1903,47 @@ describe('Task state machine', () => {
1684
1903
  expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1685
1904
  });
1686
1905
 
1906
+ it('downgrades to CONNECTED on PARTICIPANT_LEAVE when the current agent remains', () => {
1907
+ const service = startMachine();
1908
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
1909
+ const connectedTaskData = createSingleAgentConferenceTaskData('connected');
1910
+
1911
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1912
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1913
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1914
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1915
+
1916
+ service.send({
1917
+ type: TaskEvent.PARTICIPANT_LEAVE,
1918
+ participantId: 'agent-2',
1919
+ taskData: connectedTaskData,
1920
+ });
1921
+
1922
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1923
+ });
1924
+
1925
+ it('terminates instead of downgrading when PARTICIPANT_LEAVE identifies the current agent', () => {
1926
+ const service = startMachine();
1927
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
1928
+ const connectedTaskData = {
1929
+ ...createSingleAgentConferenceTaskData('connected'),
1930
+ wrapUpRequired: false,
1931
+ };
1932
+
1933
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1934
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1935
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1936
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1937
+
1938
+ service.send({
1939
+ type: TaskEvent.PARTICIPANT_LEAVE,
1940
+ participantId: 'agent-1',
1941
+ taskData: connectedTaskData,
1942
+ });
1943
+
1944
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
1945
+ });
1946
+
1687
1947
  it('returns to CONNECTED when CTQ cancel arrives before queue connects', () => {
1688
1948
  const service = startMachine();
1689
1949
  const taskData = createTaskData();
@@ -1820,10 +2080,8 @@ describe('Task state machine', () => {
1820
2080
  service.send({type: TaskEvent.CONSULT_END, taskData});
1821
2081
  expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1822
2082
  });
1823
-
1824
2083
  });
1825
2084
 
1826
-
1827
2085
  describe('CONFERENCING state CONSULT_FAILED ANOTHER_CONSULT_IN_PROGRESS (CAI-8329)', () => {
1828
2086
  it('keeps hideBlindTransfer flag and hides transfer after secondary consult rejected from Agent Desktop', () => {
1829
2087
  const service = startMachine();
@@ -1848,7 +2106,12 @@ describe('Task state machine', () => {
1848
2106
  consultState: 'conferencing',
1849
2107
  },
1850
2108
  '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
1851
- '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: false},
2109
+ '+14696762938': {
2110
+ id: '+14696762938',
2111
+ pType: 'Customer',
2112
+ hasJoined: true,
2113
+ hasLeft: false,
2114
+ },
1852
2115
  },
1853
2116
  media: {
1854
2117
  'interaction-1': {
@@ -1902,7 +2165,12 @@ describe('Task state machine', () => {
1902
2165
  consultState: 'conferencing',
1903
2166
  },
1904
2167
  '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
1905
- '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: false},
2168
+ '+14696762938': {
2169
+ id: '+14696762938',
2170
+ pType: 'Customer',
2171
+ hasJoined: true,
2172
+ hasLeft: false,
2173
+ },
1906
2174
  },
1907
2175
  media: {
1908
2176
  'interaction-1': {
@@ -1920,12 +2188,19 @@ describe('Task state machine', () => {
1920
2188
  },
1921
2189
  } as any,
1922
2190
  });
1923
- service.send({type: TaskEvent.CONSULT_FAILED, taskData: consultFailedTaskData, reason: 'ANOTHER_CONSULT_IN_PROGRESS'});
2191
+ service.send({
2192
+ type: TaskEvent.CONSULT_FAILED,
2193
+ taskData: consultFailedTaskData,
2194
+ reason: 'ANOTHER_CONSULT_IN_PROGRESS',
2195
+ });
1924
2196
 
1925
2197
  const snapshot = service.getSnapshot();
1926
2198
  expect(snapshot.value).toBe(TaskState.CONFERENCING);
1927
2199
  expect(snapshot.context.hideBlindTransferForEpDnPendingMerge).toBe(true);
1928
- expect(snapshot.context.uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
2200
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
2201
+ isVisible: false,
2202
+ isEnabled: false,
2203
+ });
1929
2204
  expect(snapshot.context.uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
1930
2205
  });
1931
2206
  });
@@ -2126,6 +2401,50 @@ describe('Task state machine', () => {
2126
2401
  service.send({type: TaskEvent.CONSULT_FAILED, taskData});
2127
2402
  expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2128
2403
  });
2404
+
2405
+ it('emits only consult-end when an unaccepted consulted offer is ended', () => {
2406
+ const emitTaskConsultEnd = jest.fn();
2407
+ const emitTaskEnd = jest.fn();
2408
+ const service = createActor(
2409
+ createTaskStateMachine(createConfig(), {
2410
+ actions: {emitTaskConsultEnd, emitTaskEnd},
2411
+ })
2412
+ );
2413
+ service.start();
2414
+ const taskData = createTaskData({isConsulted: true, consultingAgentId: 'agent-1'});
2415
+
2416
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2417
+ service.send({type: TaskEvent.CONSULT_END, taskData});
2418
+
2419
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2420
+ expect(emitTaskConsultEnd).toHaveBeenCalledTimes(1);
2421
+ expect(emitTaskEnd).not.toHaveBeenCalled();
2422
+ });
2423
+
2424
+ it('transitions to WRAPPING_UP on TASK_WRAPUP when wrapup is required', () => {
2425
+ const service = startMachine();
2426
+ const offerTaskData = createTaskData({
2427
+ interaction: {
2428
+ outboundType: 'OUTDIAL',
2429
+ } as any,
2430
+ });
2431
+
2432
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2433
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2434
+
2435
+ const wrapupTaskData = createTaskData({
2436
+ agentId: 'agent-1',
2437
+ agentsPendingWrapUp: ['agent-1'],
2438
+ wrapUpRequired: true,
2439
+ interaction: {
2440
+ outboundType: 'OUTDIAL',
2441
+ state: 'wrapUp',
2442
+ isTerminated: true,
2443
+ } as any,
2444
+ });
2445
+ service.send({type: TaskEvent.TASK_WRAPUP, taskData: wrapupTaskData});
2446
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2447
+ });
2129
2448
  });
2130
2449
 
2131
2450
  describe('OUTBOUND_FAILED handling', () => {
@@ -2161,7 +2480,11 @@ describe('Task state machine', () => {
2161
2480
  isTerminated: true,
2162
2481
  } as any,
2163
2482
  });
2164
- service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
2483
+ service.send({
2484
+ type: TaskEvent.OUTBOUND_FAILED,
2485
+ taskData: failedTaskData,
2486
+ reason: 'CUSTOMER_BUSY',
2487
+ });
2165
2488
  expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2166
2489
  });
2167
2490
 
@@ -2184,23 +2507,235 @@ describe('Task state machine', () => {
2184
2507
  isTerminated: true,
2185
2508
  } as any,
2186
2509
  });
2187
- service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
2510
+ service.send({
2511
+ type: TaskEvent.OUTBOUND_FAILED,
2512
+ taskData: failedTaskData,
2513
+ reason: 'CUSTOMER_BUSY',
2514
+ });
2188
2515
  expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2189
2516
  });
2190
- });
2191
2517
 
2192
- describe('CONSULTING state TRANSFER_CONFERENCE_SUCCESS desktop-initiated (CAI-8329)', () => {
2193
- const startAgent2Machine = () => {
2194
- const actor = createActor(
2195
- createTaskStateMachine({...createConfig(), agentId: 'agent-2'})
2196
- );
2197
- actor.start();
2518
+ it('transitions from CONNECTED to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2519
+ const service = startMachine();
2520
+ const offerTaskData = createTaskData({
2521
+ interaction: {
2522
+ outboundType: 'OUTDIAL',
2523
+ } as any,
2524
+ });
2198
2525
 
2199
- return actor;
2200
- };
2526
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2527
+ service.send({type: TaskEvent.ASSIGN, taskData: offerTaskData});
2528
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
2201
2529
 
2202
- const createSecondaryAgentConferenceBaseTaskData = () =>
2203
- createTaskData({
2530
+ const failedTaskData = createTaskData({
2531
+ agentId: 'agent-1',
2532
+ agentsPendingWrapUp: ['agent-1'],
2533
+ interaction: {
2534
+ outboundType: 'OUTDIAL',
2535
+ isTerminated: true,
2536
+ } as any,
2537
+ });
2538
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
2539
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2540
+ });
2541
+
2542
+ it('transitions from HELD to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2543
+ const service = startMachine();
2544
+ const taskData = createTaskData({
2545
+ interaction: {
2546
+ outboundType: 'OUTDIAL',
2547
+ } as any,
2548
+ });
2549
+
2550
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2551
+ service.send({type: TaskEvent.ASSIGN, taskData});
2552
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
2553
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
2554
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
2555
+
2556
+ const failedTaskData = createTaskData({
2557
+ agentId: 'agent-1',
2558
+ agentsPendingWrapUp: ['agent-1'],
2559
+ interaction: {
2560
+ outboundType: 'OUTDIAL',
2561
+ isTerminated: true,
2562
+ } as any,
2563
+ });
2564
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
2565
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2566
+ });
2567
+
2568
+ it('transitions from CONSULTING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2569
+ const service = startMachine();
2570
+ const taskData = createTaskData({
2571
+ interaction: {
2572
+ outboundType: 'OUTDIAL',
2573
+ } as any,
2574
+ });
2575
+
2576
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2577
+ service.send({type: TaskEvent.ASSIGN, taskData});
2578
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
2579
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2580
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
2581
+
2582
+ const failedTaskData = createTaskData({
2583
+ agentId: 'agent-1',
2584
+ agentsPendingWrapUp: ['agent-1'],
2585
+ interaction: {
2586
+ outboundType: 'OUTDIAL',
2587
+ isTerminated: true,
2588
+ } as any,
2589
+ });
2590
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
2591
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2592
+ });
2593
+
2594
+ it('transitions from CONFERENCING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2595
+ const service = startMachine();
2596
+ const taskData = createTaskData({
2597
+ consultingAgentId: 'agent-1',
2598
+ interaction: {
2599
+ outboundType: 'OUTDIAL',
2600
+ } as any,
2601
+ });
2602
+
2603
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2604
+ service.send({type: TaskEvent.ASSIGN, taskData});
2605
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
2606
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2607
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2608
+ service.send({type: TaskEvent.CONFERENCE_START});
2609
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2610
+
2611
+ const failedTaskData = createTaskData({
2612
+ agentId: 'agent-1',
2613
+ agentsPendingWrapUp: ['agent-1'],
2614
+ interaction: {
2615
+ outboundType: 'OUTDIAL',
2616
+ isTerminated: true,
2617
+ } as any,
2618
+ });
2619
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
2620
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2621
+ });
2622
+
2623
+ it('transitions from HOLD_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2624
+ const service = startMachine();
2625
+ const taskData = createTaskData({
2626
+ interaction: {
2627
+ outboundType: 'OUTDIAL',
2628
+ } as any,
2629
+ });
2630
+
2631
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2632
+ service.send({type: TaskEvent.ASSIGN, taskData});
2633
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
2634
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
2635
+
2636
+ const failedTaskData = createTaskData({
2637
+ agentId: 'agent-1',
2638
+ agentsPendingWrapUp: ['agent-1'],
2639
+ interaction: {
2640
+ outboundType: 'OUTDIAL',
2641
+ isTerminated: true,
2642
+ } as any,
2643
+ });
2644
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
2645
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2646
+ });
2647
+
2648
+ it('transitions from RESUME_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2649
+ const service = startMachine();
2650
+ const taskData = createTaskData({
2651
+ interaction: {
2652
+ outboundType: 'OUTDIAL',
2653
+ } as any,
2654
+ });
2655
+
2656
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2657
+ service.send({type: TaskEvent.ASSIGN, taskData});
2658
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
2659
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
2660
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
2661
+ expect(service.getSnapshot().value).toBe(TaskState.RESUME_INITIATING);
2662
+
2663
+ const failedTaskData = createTaskData({
2664
+ agentId: 'agent-1',
2665
+ agentsPendingWrapUp: ['agent-1'],
2666
+ interaction: {
2667
+ outboundType: 'OUTDIAL',
2668
+ isTerminated: true,
2669
+ } as any,
2670
+ });
2671
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
2672
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2673
+ });
2674
+
2675
+ it('transitions from CONSULT_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2676
+ const service = startMachine();
2677
+ const taskData = createTaskData({
2678
+ interaction: {
2679
+ outboundType: 'OUTDIAL',
2680
+ } as any,
2681
+ });
2682
+
2683
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2684
+ service.send({type: TaskEvent.ASSIGN, taskData});
2685
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
2686
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
2687
+
2688
+ const failedTaskData = createTaskData({
2689
+ agentId: 'agent-1',
2690
+ agentsPendingWrapUp: ['agent-1'],
2691
+ interaction: {
2692
+ outboundType: 'OUTDIAL',
2693
+ isTerminated: true,
2694
+ } as any,
2695
+ });
2696
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
2697
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2698
+ });
2699
+
2700
+ it('transitions from CONF_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2701
+ const service = startMachine();
2702
+ const taskData = createTaskData({
2703
+ consultingAgentId: 'agent-1',
2704
+ interaction: {
2705
+ outboundType: 'OUTDIAL',
2706
+ } as any,
2707
+ });
2708
+
2709
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2710
+ service.send({type: TaskEvent.ASSIGN, taskData});
2711
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
2712
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2713
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2714
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
2715
+
2716
+ const failedTaskData = createTaskData({
2717
+ agentId: 'agent-1',
2718
+ agentsPendingWrapUp: ['agent-1'],
2719
+ interaction: {
2720
+ outboundType: 'OUTDIAL',
2721
+ isTerminated: true,
2722
+ } as any,
2723
+ });
2724
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
2725
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2726
+ });
2727
+ });
2728
+
2729
+ describe('CONSULTING state TRANSFER_CONFERENCE_SUCCESS desktop-initiated (CAI-8329)', () => {
2730
+ const startAgent2Machine = () => {
2731
+ const actor = createActor(createTaskStateMachine({...createConfig(), agentId: 'agent-2'}));
2732
+ actor.start();
2733
+
2734
+ return actor;
2735
+ };
2736
+
2737
+ const createSecondaryAgentConferenceBaseTaskData = () =>
2738
+ createTaskData({
2204
2739
  agentId: 'agent-2',
2205
2740
  isConsulted: true,
2206
2741
  interactionId: 'interaction-1',
@@ -2292,7 +2827,54 @@ describe('Task state machine', () => {
2292
2827
  } as any,
2293
2828
  });
2294
2829
 
2295
- const setupSecondaryAgentConsultingFromConference = (service: ReturnType<typeof startAgent2Machine>) => {
2830
+ const createSecondaryAgentRemovedTaskData = () =>
2831
+ createTaskData({
2832
+ agentId: 'agent-2',
2833
+ isConsulted: true,
2834
+ interactionId: 'interaction-1',
2835
+ mediaResourceId: 'interaction-1',
2836
+ consultMediaResourceId: 'consult-media-1',
2837
+ interaction: {
2838
+ state: 'conference',
2839
+ owner: 'agent-1',
2840
+ mainInteractionId: 'interaction-1',
2841
+ interactionId: 'interaction-1',
2842
+ participants: {
2843
+ 'agent-1': {
2844
+ id: 'agent-1',
2845
+ pType: 'Agent',
2846
+ hasLeft: false,
2847
+ consultState: 'conferencing',
2848
+ },
2849
+ 'agent-3': {
2850
+ id: 'agent-3',
2851
+ pType: 'Agent',
2852
+ hasLeft: false,
2853
+ isConsulted: true,
2854
+ consultState: 'consulting',
2855
+ },
2856
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2857
+ },
2858
+ media: {
2859
+ 'arbitrary-main-media-key': {
2860
+ mediaResourceId: 'interaction-1',
2861
+ mType: 'mainCall',
2862
+ participants: ['agent-1', 'customer-1'],
2863
+ isHold: false,
2864
+ },
2865
+ 'consult-media-1': {
2866
+ mediaResourceId: 'consult-media-1',
2867
+ mType: 'consult',
2868
+ participants: ['agent-2', 'agent-3'],
2869
+ isHold: false,
2870
+ },
2871
+ },
2872
+ } as any,
2873
+ });
2874
+
2875
+ const setupSecondaryAgentConsultingFromConference = (
2876
+ service: ReturnType<typeof startAgent2Machine>
2877
+ ) => {
2296
2878
  const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
2297
2879
  const consultingTaskData = createSecondaryAgentConsultingTaskData();
2298
2880
 
@@ -2339,7 +2921,10 @@ describe('Task state machine', () => {
2339
2921
  setupSecondaryAgentDesktopConsultingFromConference(service);
2340
2922
 
2341
2923
  const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
2342
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
2924
+ service.send({
2925
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
2926
+ taskData: transferSuccessTaskData,
2927
+ });
2343
2928
 
2344
2929
  expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2345
2930
  });
@@ -2349,7 +2934,10 @@ describe('Task state machine', () => {
2349
2934
  setupSecondaryAgentConsultingFromConference(service);
2350
2935
 
2351
2936
  const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
2352
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
2937
+ service.send({
2938
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
2939
+ taskData: transferSuccessTaskData,
2940
+ });
2353
2941
 
2354
2942
  const snapshot = service.getSnapshot();
2355
2943
  expect(snapshot.value).toBe(TaskState.TERMINATED);
@@ -2359,9 +2947,7 @@ describe('Task state machine', () => {
2359
2947
 
2360
2948
  it('moves consultee to CONFERENCING when primary agent conference-transfers', () => {
2361
2949
  const startAgent3Machine = () => {
2362
- const actor = createActor(
2363
- createTaskStateMachine({...createConfig(), agentId: 'agent-3'})
2364
- );
2950
+ const actor = createActor(createTaskStateMachine({...createConfig(), agentId: 'agent-3'}));
2365
2951
  actor.start();
2366
2952
 
2367
2953
  return actor;
@@ -2524,7 +3110,10 @@ describe('Task state machine', () => {
2524
3110
  expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
2525
3111
  expect(service.getSnapshot().context.consultInitiator).toBe(false);
2526
3112
 
2527
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
3113
+ service.send({
3114
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3115
+ taskData: transferSuccessTaskData,
3116
+ });
2528
3117
  expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2529
3118
  });
2530
3119
 
@@ -2582,7 +3171,10 @@ describe('Task state machine', () => {
2582
3171
  expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2583
3172
  expect(service.getSnapshot().context.consultInitiator).toBe(false);
2584
3173
 
2585
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
3174
+ service.send({
3175
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3176
+ taskData: transferSuccessTaskData,
3177
+ });
2586
3178
  expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2587
3179
  });
2588
3180
 
@@ -2592,7 +3184,10 @@ describe('Task state machine', () => {
2592
3184
 
2593
3185
  const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
2594
3186
  service.send({type: TaskEvent.TRANSFER_CONFERENCE});
2595
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
3187
+ service.send({
3188
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3189
+ taskData: transferSuccessTaskData,
3190
+ });
2596
3191
 
2597
3192
  expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2598
3193
  });
@@ -2647,5 +3242,192 @@ describe('Task state machine', () => {
2647
3242
 
2648
3243
  expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2649
3244
  });
3245
+
3246
+ it('terminates secondary agent on PARTICIPANT_LEAVE while consult is initiating', () => {
3247
+ const service = startAgent2Machine();
3248
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
3249
+
3250
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3251
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3252
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3253
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
3254
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
3255
+ expect(service.getSnapshot().context.taskData.interaction).toBe(
3256
+ conferenceTaskData.interaction
3257
+ );
3258
+ expect(service.getSnapshot().context.taskData.destAgentId).toBe('agent-3');
3259
+
3260
+ const participantLeaveEvent = {
3261
+ type: TaskEvent.PARTICIPANT_LEAVE,
3262
+ taskData: createSecondaryAgentRemovedTaskData(),
3263
+ } as const;
3264
+ expect(
3265
+ guards.didCurrentAgentLeaveMainInteraction({
3266
+ context: service.getSnapshot().context,
3267
+ event: participantLeaveEvent,
3268
+ })
3269
+ ).toBe(true);
3270
+
3271
+ service.send(participantLeaveEvent);
3272
+
3273
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3274
+ });
3275
+
3276
+ it('terminates secondary agent on CONSULT_END while consult is initiating', () => {
3277
+ const service = startAgent2Machine();
3278
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
3279
+
3280
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3281
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3282
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3283
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
3284
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
3285
+
3286
+ const consultEndEvent = {
3287
+ type: TaskEvent.CONSULT_END,
3288
+ taskData: createSecondaryAgentRemovedTaskData(),
3289
+ } as const;
3290
+ expect(
3291
+ guards.didCurrentAgentLeaveMainInteraction({
3292
+ context: service.getSnapshot().context,
3293
+ event: consultEndEvent,
3294
+ })
3295
+ ).toBe(true);
3296
+
3297
+ service.send(consultEndEvent);
3298
+
3299
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3300
+ });
3301
+
3302
+ it('terminates a consulting secondary initiator when CONSULT_END removes it from mainCall', () => {
3303
+ const service = startAgent2Machine();
3304
+ setupSecondaryAgentConsultingFromConference(service);
3305
+
3306
+ service.send({
3307
+ type: TaskEvent.CONSULT_END,
3308
+ taskData: createSecondaryAgentRemovedTaskData(),
3309
+ });
3310
+
3311
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3312
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
3313
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(false);
3314
+ });
3315
+
3316
+ it('does not infer departure from a partial CONSULT_END payload', () => {
3317
+ const service = startAgent2Machine();
3318
+ setupSecondaryAgentConsultingFromConference(service);
3319
+ const partialTaskData = {
3320
+ ...createTaskData({agentId: 'agent-2', interactionId: 'interaction-1'}),
3321
+ interaction: {
3322
+ state: 'conference',
3323
+ interactionId: 'interaction-1',
3324
+ mainInteractionId: 'interaction-1',
3325
+ },
3326
+ } as any;
3327
+
3328
+ const consultEndEvent = {type: TaskEvent.CONSULT_END, taskData: partialTaskData} as const;
3329
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
3330
+ expect(
3331
+ guards.didCurrentAgentLeaveMainInteraction({
3332
+ context: service.getSnapshot().context,
3333
+ event: consultEndEvent,
3334
+ })
3335
+ ).toBe(false);
3336
+
3337
+ service.send(consultEndEvent);
3338
+
3339
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
3340
+ });
3341
+
3342
+ it('emits consult-end and task-end when AgentConsultEnded terminates the consulted agent', () => {
3343
+ const emitTaskConsultEnd = jest.fn();
3344
+ const emitTaskEnd = jest.fn();
3345
+ const cleanupResources = jest.fn();
3346
+ const service = createActor(
3347
+ createTaskStateMachine(
3348
+ {...createConfig(), agentId: 'agent-3'},
3349
+ {
3350
+ actions: {emitTaskConsultEnd, emitTaskEnd, cleanupResources},
3351
+ }
3352
+ )
3353
+ );
3354
+ service.start();
3355
+
3356
+ const consultingTaskData = createTaskData({
3357
+ agentId: 'agent-3',
3358
+ isConsulted: true,
3359
+ interactionId: 'consult-child',
3360
+ interaction: {
3361
+ state: 'consulting',
3362
+ owner: 'agent-1',
3363
+ interactionId: 'consult-child',
3364
+ mainInteractionId: 'interaction-1',
3365
+ participants: {
3366
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
3367
+ 'agent-2': {
3368
+ id: 'agent-2',
3369
+ pType: 'Agent',
3370
+ hasLeft: false,
3371
+ consultState: 'consulting',
3372
+ },
3373
+ 'agent-3': {
3374
+ id: 'agent-3',
3375
+ pType: 'Agent',
3376
+ hasLeft: false,
3377
+ isConsulted: true,
3378
+ consultState: 'consulting',
3379
+ },
3380
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3381
+ },
3382
+ media: {
3383
+ 'interaction-1': {
3384
+ mediaResourceId: 'interaction-1',
3385
+ mType: 'mainCall',
3386
+ participants: ['agent-1', 'agent-2', 'customer-1'],
3387
+ isHold: true,
3388
+ },
3389
+ 'consult-child': {
3390
+ mediaResourceId: 'consult-child',
3391
+ mType: 'consult',
3392
+ participants: ['agent-2', 'agent-3'],
3393
+ isHold: false,
3394
+ },
3395
+ },
3396
+ } as any,
3397
+ });
3398
+ service.send({type: TaskEvent.HYDRATE, taskData: consultingTaskData});
3399
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
3400
+
3401
+ const consultEndedTaskData = createTaskData({
3402
+ ...consultingTaskData,
3403
+ type: 'AgentConsultEnded' as any,
3404
+ interaction: {
3405
+ ...consultingTaskData.interaction,
3406
+ state: 'conference',
3407
+ participants: {
3408
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
3409
+ 'agent-2': {id: 'agent-2', pType: 'Agent', hasLeft: false},
3410
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3411
+ },
3412
+ media: {
3413
+ 'interaction-1': {
3414
+ mediaResourceId: 'interaction-1',
3415
+ mType: 'mainCall',
3416
+ participants: ['agent-1', 'agent-2', 'customer-1'],
3417
+ isHold: false,
3418
+ },
3419
+ },
3420
+ } as any,
3421
+ });
3422
+
3423
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
3424
+
3425
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3426
+ expect(emitTaskConsultEnd).toHaveBeenCalledTimes(1);
3427
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
3428
+ expect(cleanupResources).toHaveBeenCalledTimes(1);
3429
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(false);
3430
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
3431
+ });
2650
3432
  });
2651
3433
  });