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

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 (267) hide show
  1. package/.sdd/manifest.json +882 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +46 -0
  5. package/ai-docs/GETTING_STARTED.md +168 -0
  6. package/ai-docs/GLOSSARY.md +43 -0
  7. package/ai-docs/README.md +138 -0
  8. package/ai-docs/REVIEW_CHECKLIST.md +41 -0
  9. package/ai-docs/RULES.md +444 -0
  10. package/ai-docs/SECURITY.md +52 -0
  11. package/ai-docs/SERVICE_STATE.md +48 -0
  12. package/ai-docs/SPEC_INDEX.md +65 -0
  13. package/ai-docs/adr/0001-spec-source-policy.md +55 -0
  14. package/ai-docs/adr/README.md +8 -0
  15. package/ai-docs/adr/_adr-template.md +31 -0
  16. package/ai-docs/contact-center-spec.md +341 -0
  17. package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
  18. package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
  19. package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
  20. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  21. package/ai-docs/patterns/testing-patterns.md +480 -0
  22. package/ai-docs/patterns/typescript-patterns.md +365 -0
  23. package/ai-docs/templates/README.md +102 -0
  24. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  25. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  26. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  27. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  28. package/ai-docs/templates/new-method/00-master.md +80 -0
  29. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  30. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  31. package/ai-docs/templates/new-method/03-tests.md +201 -0
  32. package/ai-docs/templates/new-method/04-validation.md +141 -0
  33. package/ai-docs/templates/new-service/00-master.md +109 -0
  34. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  35. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  36. package/ai-docs/templates/new-service/03-integration.md +178 -0
  37. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  38. package/ai-docs/templates/new-service/05-validation.md +145 -0
  39. package/dist/cc.js +379 -50
  40. package/dist/cc.js.map +1 -1
  41. package/dist/config.js +6 -0
  42. package/dist/config.js.map +1 -1
  43. package/dist/constants.js +22 -2
  44. package/dist/constants.js.map +1 -1
  45. package/dist/index.js +27 -5
  46. package/dist/index.js.map +1 -1
  47. package/dist/metrics/behavioral-events.js +114 -0
  48. package/dist/metrics/behavioral-events.js.map +1 -1
  49. package/dist/metrics/constants.js +32 -3
  50. package/dist/metrics/constants.js.map +1 -1
  51. package/dist/services/ApiAiAssistant.js +318 -0
  52. package/dist/services/ApiAiAssistant.js.map +1 -0
  53. package/dist/services/UserPreference.js +427 -0
  54. package/dist/services/UserPreference.js.map +1 -0
  55. package/dist/services/agent/types.js.map +1 -1
  56. package/dist/services/config/Util.js +8 -4
  57. package/dist/services/config/Util.js.map +1 -1
  58. package/dist/services/config/constants.js +35 -2
  59. package/dist/services/config/constants.js.map +1 -1
  60. package/dist/services/config/index.js +41 -2
  61. package/dist/services/config/index.js.map +1 -1
  62. package/dist/services/config/types.js +66 -8
  63. package/dist/services/config/types.js.map +1 -1
  64. package/dist/services/constants.js +27 -1
  65. package/dist/services/constants.js.map +1 -1
  66. package/dist/services/core/Err.js.map +1 -1
  67. package/dist/services/core/Utils.js +122 -25
  68. package/dist/services/core/Utils.js.map +1 -1
  69. package/dist/services/core/aqm-reqs.js +92 -17
  70. package/dist/services/core/aqm-reqs.js.map +1 -1
  71. package/dist/services/core/websocket/WebSocketManager.js +22 -6
  72. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  73. package/dist/services/core/websocket/connection-service.js +3 -1
  74. package/dist/services/core/websocket/connection-service.js.map +1 -1
  75. package/dist/services/core/websocket/types.js.map +1 -1
  76. package/dist/services/index.js +6 -0
  77. package/dist/services/index.js.map +1 -1
  78. package/dist/services/task/Task.js +688 -0
  79. package/dist/services/task/Task.js.map +1 -0
  80. package/dist/services/task/TaskFactory.js +45 -0
  81. package/dist/services/task/TaskFactory.js.map +1 -0
  82. package/dist/services/task/TaskManager.js +751 -457
  83. package/dist/services/task/TaskManager.js.map +1 -1
  84. package/dist/services/task/TaskUtils.js +220 -23
  85. package/dist/services/task/TaskUtils.js.map +1 -1
  86. package/dist/services/task/constants.js +23 -2
  87. package/dist/services/task/constants.js.map +1 -1
  88. package/dist/services/task/dialer.js +129 -0
  89. package/dist/services/task/dialer.js.map +1 -1
  90. package/dist/services/task/digital/Digital.js +77 -0
  91. package/dist/services/task/digital/Digital.js.map +1 -0
  92. package/dist/services/task/state-machine/TaskStateMachine.js +873 -0
  93. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  94. package/dist/services/task/state-machine/actions.js +567 -0
  95. package/dist/services/task/state-machine/actions.js.map +1 -0
  96. package/dist/services/task/state-machine/constants.js +161 -0
  97. package/dist/services/task/state-machine/constants.js.map +1 -0
  98. package/dist/services/task/state-machine/guards.js +382 -0
  99. package/dist/services/task/state-machine/guards.js.map +1 -0
  100. package/dist/services/task/state-machine/index.js +53 -0
  101. package/dist/services/task/state-machine/index.js.map +1 -0
  102. package/dist/services/task/state-machine/types.js +54 -0
  103. package/dist/services/task/state-machine/types.js.map +1 -0
  104. package/dist/services/task/state-machine/uiControlsComputer.js +603 -0
  105. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  106. package/dist/services/task/taskDataNormalizer.js +99 -0
  107. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  108. package/dist/services/task/types.js +227 -4
  109. package/dist/services/task/types.js.map +1 -1
  110. package/dist/services/task/voice/Voice.js +1044 -0
  111. package/dist/services/task/voice/Voice.js.map +1 -0
  112. package/dist/services/task/voice/WebRTC.js +149 -0
  113. package/dist/services/task/voice/WebRTC.js.map +1 -0
  114. package/dist/types/cc.d.ts +894 -0
  115. package/dist/types/config.d.ts +72 -0
  116. package/dist/types/constants.d.ts +66 -0
  117. package/dist/types/index.d.ts +199 -0
  118. package/dist/types/logger-proxy.d.ts +71 -0
  119. package/dist/types/metrics/MetricsManager.d.ts +223 -0
  120. package/dist/types/metrics/behavioral-events.d.ts +29 -0
  121. package/dist/types/metrics/constants.d.ts +181 -0
  122. package/dist/types/services/AddressBook.d.ts +74 -0
  123. package/dist/types/services/ApiAiAssistant.d.ts +49 -0
  124. package/dist/types/services/EntryPoint.d.ts +67 -0
  125. package/dist/types/services/Queue.d.ts +76 -0
  126. package/dist/types/services/UserPreference.d.ts +118 -0
  127. package/dist/types/services/WebCallingService.d.ts +1 -0
  128. package/dist/types/services/agent/index.d.ts +46 -0
  129. package/dist/types/services/agent/types.d.ts +413 -0
  130. package/dist/types/services/config/Util.d.ts +20 -0
  131. package/dist/types/services/config/constants.d.ts +270 -0
  132. package/dist/types/services/config/index.d.ts +177 -0
  133. package/dist/types/services/config/types.d.ts +1368 -0
  134. package/dist/types/services/constants.d.ts +110 -0
  135. package/dist/types/services/core/Err.d.ts +125 -0
  136. package/dist/types/services/core/GlobalTypes.d.ts +58 -0
  137. package/dist/types/services/core/Utils.d.ts +121 -0
  138. package/dist/types/services/core/WebexRequest.d.ts +22 -0
  139. package/dist/types/services/core/aqm-reqs.d.ts +65 -0
  140. package/dist/types/services/core/constants.d.ts +99 -0
  141. package/dist/types/services/core/types.d.ts +47 -0
  142. package/dist/types/services/core/websocket/WebSocketManager.d.ts +36 -0
  143. package/dist/types/services/core/websocket/connection-service.d.ts +27 -0
  144. package/dist/types/services/core/websocket/keepalive.worker.d.ts +2 -0
  145. package/dist/types/services/core/websocket/types.d.ts +37 -0
  146. package/dist/types/services/index.d.ts +54 -0
  147. package/dist/types/services/task/AutoWrapup.d.ts +40 -0
  148. package/dist/types/services/task/Task.d.ts +157 -0
  149. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  150. package/dist/types/services/task/TaskManager.d.ts +1 -0
  151. package/dist/types/services/task/TaskUtils.d.ts +138 -0
  152. package/dist/types/services/task/constants.d.ts +91 -0
  153. package/dist/types/services/task/contact.d.ts +69 -0
  154. package/dist/types/services/task/dialer.d.ts +73 -0
  155. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  156. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1194 -0
  157. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  158. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  159. package/dist/types/services/task/state-machine/guards.d.ts +102 -0
  160. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  161. package/dist/types/services/task/state-machine/types.d.ts +269 -0
  162. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  163. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  164. package/dist/types/services/task/types.d.ts +1856 -0
  165. package/dist/types/services/task/voice/Voice.d.ts +184 -0
  166. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  167. package/dist/types/types.d.ts +778 -0
  168. package/dist/types/utils/PageCache.d.ts +173 -0
  169. package/dist/types/webex-config.d.ts +53 -0
  170. package/dist/types/webex.d.ts +8 -0
  171. package/dist/types.js +130 -1
  172. package/dist/types.js.map +1 -1
  173. package/dist/webex.js +14 -2
  174. package/dist/webex.js.map +1 -1
  175. package/package.json +16 -12
  176. package/src/cc.ts +477 -51
  177. package/src/config.ts +6 -0
  178. package/src/constants.ts +21 -1
  179. package/src/index.ts +24 -5
  180. package/src/metrics/ai-docs/AGENTS.md +350 -0
  181. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  182. package/src/metrics/ai-docs/metrics-spec.md +854 -0
  183. package/src/metrics/behavioral-events.ts +120 -0
  184. package/src/metrics/constants.ts +37 -3
  185. package/src/services/ApiAiAssistant.ts +412 -0
  186. package/src/services/UserPreference.ts +509 -0
  187. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  188. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  189. package/src/services/agent/ai-docs/agent-spec.md +504 -0
  190. package/src/services/agent/types.ts +1 -1
  191. package/src/services/ai-docs/AGENTS.md +386 -0
  192. package/src/services/ai-docs/services-spec.md +492 -0
  193. package/src/services/config/Util.ts +10 -2
  194. package/src/services/config/ai-docs/AGENTS.md +255 -0
  195. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  196. package/src/services/config/ai-docs/config-spec.md +669 -0
  197. package/src/services/config/constants.ts +37 -1
  198. package/src/services/config/index.ts +45 -1
  199. package/src/services/config/types.ts +241 -11
  200. package/src/services/constants.ts +29 -0
  201. package/src/services/core/Err.ts +3 -0
  202. package/src/services/core/Utils.ts +143 -30
  203. package/src/services/core/ai-docs/AGENTS.md +381 -0
  204. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  205. package/src/services/core/ai-docs/core-spec.md +783 -0
  206. package/src/services/core/aqm-reqs.ts +100 -22
  207. package/src/services/core/websocket/WebSocketManager.ts +23 -6
  208. package/src/services/core/websocket/connection-service.ts +5 -1
  209. package/src/services/core/websocket/types.ts +1 -1
  210. package/src/services/index.ts +4 -0
  211. package/src/services/task/Task.ts +837 -0
  212. package/src/services/task/TaskFactory.ts +55 -0
  213. package/src/services/task/TaskManager.ts +793 -521
  214. package/src/services/task/TaskUtils.ts +314 -24
  215. package/src/services/task/ai-docs/AGENTS.md +457 -0
  216. package/src/services/task/ai-docs/ARCHITECTURE.md +594 -0
  217. package/src/services/task/ai-docs/task-spec.md +1319 -0
  218. package/src/services/task/constants.ts +23 -0
  219. package/src/services/task/dialer.ts +136 -1
  220. package/src/services/task/digital/Digital.ts +95 -0
  221. package/src/services/task/state-machine/TaskStateMachine.ts +1166 -0
  222. package/src/services/task/state-machine/actions.ts +738 -0
  223. package/src/services/task/state-machine/ai-docs/AGENTS.md +458 -0
  224. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1137 -0
  225. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2177 -0
  226. package/src/services/task/state-machine/constants.ts +172 -0
  227. package/src/services/task/state-machine/guards.ts +445 -0
  228. package/src/services/task/state-machine/index.ts +28 -0
  229. package/src/services/task/state-machine/types.ts +243 -0
  230. package/src/services/task/state-machine/uiControlsComputer.ts +961 -0
  231. package/src/services/task/taskDataNormalizer.ts +137 -0
  232. package/src/services/task/types.ts +734 -71
  233. package/src/services/task/voice/Voice.ts +1270 -0
  234. package/src/services/task/voice/WebRTC.ts +187 -0
  235. package/src/types.ts +205 -2
  236. package/src/utils/AGENTS.md +278 -0
  237. package/src/utils/ai-docs/utils-spec.md +381 -0
  238. package/src/webex.js +2 -0
  239. package/test/unit/spec/cc.ts +503 -43
  240. package/test/unit/spec/logger-proxy.ts +70 -0
  241. package/test/unit/spec/services/ApiAiAssistant.ts +273 -0
  242. package/test/unit/spec/services/UserPreference.ts +401 -0
  243. package/test/unit/spec/services/WebCallingService.ts +7 -1
  244. package/test/unit/spec/services/config/index.ts +85 -29
  245. package/test/unit/spec/services/core/Utils.ts +481 -2
  246. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +137 -41
  247. package/test/unit/spec/services/core/websocket/connection-service.ts +3 -1
  248. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  249. package/test/unit/spec/services/task/Task.ts +477 -0
  250. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  251. package/test/unit/spec/services/task/TaskManager.ts +1001 -1003
  252. package/test/unit/spec/services/task/TaskUtils.ts +235 -0
  253. package/test/unit/spec/services/task/dialer.ts +372 -96
  254. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  255. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +2651 -0
  256. package/test/unit/spec/services/task/state-machine/guards.ts +637 -0
  257. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  258. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2663 -0
  259. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  260. package/test/unit/spec/services/task/voice/Voice.ts +649 -0
  261. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  262. package/umd/contact-center.min.js +2 -2
  263. package/umd/contact-center.min.js.map +1 -1
  264. package/dist/services/task/index.js +0 -1525
  265. package/dist/services/task/index.js.map +0 -1
  266. package/src/services/task/index.ts +0 -1801
  267. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -0,0 +1,2651 @@
1
+ import {createActor} from 'xstate';
2
+ import {
3
+ createTaskStateMachine,
4
+ TaskEvent,
5
+ TaskState,
6
+ } from '../../../../../../src/services/task/state-machine';
7
+ import {createTaskData} from '../taskTestUtils';
8
+
9
+ const createConfig = () => ({
10
+ channelType: 'voice' as const,
11
+ isEndTaskEnabled: true,
12
+ isEndConsultEnabled: true,
13
+ voiceVariant: 'pstn' as const,
14
+ isRecordingEnabled: true,
15
+ });
16
+
17
+ describe('Task state machine', () => {
18
+ const startMachine = () => {
19
+ const actor = createActor(createTaskStateMachine(createConfig()));
20
+ actor.start();
21
+
22
+ return actor;
23
+ };
24
+
25
+ describe('recording state derivation', () => {
26
+ it('captures recording flags from offer payload', () => {
27
+ const service = startMachine();
28
+ const taskData = createTaskData({
29
+ interaction: {
30
+ callProcessingDetails: {
31
+ recordInProgress: true,
32
+ isPaused: false,
33
+ },
34
+ } as any,
35
+ });
36
+
37
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
38
+
39
+ const snapshot = service.getSnapshot();
40
+ expect(snapshot.context.recordingControlsAvailable).toBe(true);
41
+ expect(snapshot.context.recordingInProgress).toBe(true);
42
+ });
43
+
44
+ it('updates recordingPaused when ASSIGN payload reports pause', () => {
45
+ const service = startMachine();
46
+ const initialTaskData = createTaskData();
47
+ const pausedTaskData = createTaskData({
48
+ interaction: {
49
+ callProcessingDetails: {
50
+ isPaused: true,
51
+ },
52
+ } as any,
53
+ });
54
+
55
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: initialTaskData});
56
+ service.send({type: TaskEvent.ASSIGN, taskData: pausedTaskData});
57
+
58
+ const snapshot = service.getSnapshot();
59
+ expect(snapshot.context.recordingControlsAvailable).toBe(true);
60
+ expect(snapshot.context.recordingInProgress).toBe(false);
61
+ });
62
+
63
+ it('updates recording state when recording started event arrives', () => {
64
+ const service = startMachine();
65
+ const initialTaskData = createTaskData();
66
+ const recordingTaskData = createTaskData({
67
+ interaction: {
68
+ callProcessingDetails: {
69
+ recordingStarted: true,
70
+ recordInProgress: true,
71
+ },
72
+ } as any,
73
+ });
74
+
75
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: initialTaskData});
76
+ service.send({type: TaskEvent.ASSIGN, taskData: initialTaskData});
77
+ service.send({type: TaskEvent.RECORDING_STARTED, taskData: recordingTaskData});
78
+
79
+ const snapshot = service.getSnapshot();
80
+ expect(snapshot.value).toBe(TaskState.CONNECTED);
81
+ expect(snapshot.context.recordingControlsAvailable).toBe(true);
82
+ expect(snapshot.context.recordingInProgress).toBe(true);
83
+ });
84
+ });
85
+
86
+ describe('hold and resume flow', () => {
87
+ it('moves through HOLD -> HELD -> CONNECTED on success events', () => {
88
+ const service = startMachine();
89
+ const taskData = createTaskData();
90
+
91
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
92
+ service.send({type: TaskEvent.ASSIGN, taskData});
93
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
94
+
95
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
96
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
97
+
98
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
99
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
100
+
101
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
102
+ expect(service.getSnapshot().value).toBe(TaskState.RESUME_INITIATING);
103
+
104
+ service.send({type: TaskEvent.UNHOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
105
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
106
+ });
107
+ });
108
+
109
+ describe('recording pause/resume events', () => {
110
+ it('toggles recordingPaused flag based on events', () => {
111
+ const service = startMachine();
112
+ const taskData = createTaskData({
113
+ interaction: {
114
+ callProcessingDetails: {recordInProgress: true},
115
+ } as any,
116
+ });
117
+
118
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
119
+ service.send({type: TaskEvent.ASSIGN, taskData});
120
+ expect(service.getSnapshot().context.recordingInProgress).toBe(true);
121
+
122
+ service.send({type: TaskEvent.PAUSE_RECORDING});
123
+ expect(service.getSnapshot().context.recordingInProgress).toBe(false);
124
+
125
+ service.send({type: TaskEvent.RESUME_RECORDING});
126
+ expect(service.getSnapshot().context.recordingInProgress).toBe(true);
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
+ });
150
+ });
151
+
152
+ describe('wrap-up and completion flow', () => {
153
+ it('moves from CONNECTED -> WRAPPING_UP -> COMPLETED on END/WRAPUP_COMPLETE', () => {
154
+ const service = startMachine();
155
+ const taskData = createTaskData();
156
+
157
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
158
+ service.send({type: TaskEvent.ASSIGN, taskData});
159
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
160
+
161
+ service.send({type: TaskEvent.TASK_WRAPUP});
162
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
163
+
164
+ service.send({type: TaskEvent.WRAPUP_COMPLETE});
165
+ expect(service.getSnapshot().value).toBe(TaskState.COMPLETED);
166
+ });
167
+
168
+ it('handles CONTACT_ENDED by entering wrapping up before completion', () => {
169
+ const service = startMachine();
170
+ // Primary agent (isConsulted: false) should go to WRAPPING_UP
171
+ const taskData = createTaskData({isConsulted: false} as any);
172
+
173
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
174
+ service.send({type: TaskEvent.ASSIGN, taskData});
175
+
176
+ // CONTACT_ENDED event must include taskData for shouldWrapUpForThisAgent check
177
+ service.send({type: TaskEvent.CONTACT_ENDED, taskData});
178
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
179
+
180
+ service.send({type: TaskEvent.WRAPUP_COMPLETE});
181
+ expect(service.getSnapshot().value).toBe(TaskState.COMPLETED);
182
+ });
183
+ });
184
+
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
+
275
+ it('boots from IDLE to CONSULTING on CONSULTING_ACTIVE for split-leg ordering', () => {
276
+ const service = startMachine();
277
+ const taskData = createTaskData({
278
+ consultingAgentId: 'agent-1',
279
+ isConsulted: false,
280
+ interaction: {
281
+ state: 'consulting',
282
+ } as any,
283
+ });
284
+
285
+ expect(service.getSnapshot().value).toBe(TaskState.IDLE);
286
+ service.send({
287
+ type: TaskEvent.CONSULTING_ACTIVE,
288
+ consultDestinationAgentJoined: true,
289
+ taskData,
290
+ });
291
+
292
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
293
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(true);
294
+ });
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
+
444
+ it('tracks consult destination, agent join, and clears on consult end', () => {
445
+ const service = startMachine();
446
+ const taskData = createTaskData();
447
+
448
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
449
+ service.send({type: TaskEvent.ASSIGN, taskData});
450
+
451
+ service.send({
452
+ type: TaskEvent.CONSULT,
453
+ destination: 'agent-42',
454
+ destinationType: 'agent',
455
+ });
456
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
457
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
458
+
459
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
460
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
461
+
462
+ service.send({
463
+ type: TaskEvent.CONSULTING_ACTIVE,
464
+ consultDestinationAgentJoined: true,
465
+ });
466
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(true);
467
+
468
+ service.send({type: TaskEvent.CONSULT_END});
469
+ const snapshotAfterEnd = service.getSnapshot();
470
+ expect(snapshotAfterEnd.value).toBe(TaskState.HELD);
471
+ expect(snapshotAfterEnd.context.consultDestinationAgentJoined).toBe(false);
472
+ });
473
+
474
+ it('returns to HELD with main-leg controls after AgentConsultEnded from Stable Prod while CONSULTING', () => {
475
+ const service = startMachine();
476
+ const baseTaskData = createTaskData({
477
+ agentId: 'agent-1',
478
+ mediaResourceId: 'interaction-1',
479
+ });
480
+
481
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
482
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
483
+ service.send({
484
+ type: TaskEvent.CONSULT,
485
+ destination: 'agent-2',
486
+ destinationType: 'agent',
487
+ });
488
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
489
+ service.send({type: TaskEvent.CONSULTING_ACTIVE, consultDestinationAgentJoined: true});
490
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
491
+
492
+ const consultEndedTaskData = createTaskData({
493
+ agentId: 'agent-1',
494
+ mediaResourceId: 'interaction-1',
495
+ type: 'AgentConsultEnded' as any,
496
+ isConsulted: false,
497
+ interaction: {
498
+ state: 'connected',
499
+ interactionId: 'interaction-1',
500
+ mainInteractionId: 'interaction-1',
501
+ owner: 'agent-1',
502
+ participants: {
503
+ 'agent-1': {
504
+ id: 'agent-1',
505
+ pType: 'Agent',
506
+ hasLeft: false,
507
+ consultState: 'consultCompleted',
508
+ isConsulted: false,
509
+ },
510
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
511
+ },
512
+ media: {
513
+ 'interaction-1': {
514
+ mediaResourceId: 'interaction-1',
515
+ mType: 'mainCall',
516
+ isHold: true,
517
+ participants: ['customer-1', 'agent-1'],
518
+ },
519
+ },
520
+ } as any,
521
+ });
522
+
523
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
524
+
525
+ const snapshot = service.getSnapshot();
526
+ expect(snapshot.value).toBe(TaskState.HELD);
527
+ expect(snapshot.context.consultInitiator).toBe(false);
528
+ expect(snapshot.context.consultCallHeld).toBe(false);
529
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(false);
530
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
531
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
532
+ isVisible: false,
533
+ isEnabled: false,
534
+ });
535
+ expect(snapshot.context.uiControls.main.hold).toEqual({
536
+ isVisible: true,
537
+ isEnabled: true,
538
+ });
539
+ expect(snapshot.context.uiControls.main.consult).toEqual({
540
+ isVisible: true,
541
+ isEnabled: true,
542
+ });
543
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
544
+ isVisible: true,
545
+ isEnabled: true,
546
+ });
547
+ expect(snapshot.context.uiControls.main.recording).toEqual({
548
+ isVisible: true,
549
+ isEnabled: true,
550
+ });
551
+ });
552
+
553
+ it('enables main consult after ending consult before consultee answers (CONSULT_INITIATING -> CONSULT_END)', () => {
554
+ const service = startMachine();
555
+ const baseTaskData = createTaskData({
556
+ agentId: 'agent-1',
557
+ mediaResourceId: 'interaction-1',
558
+ });
559
+
560
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
561
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
562
+ service.send({
563
+ type: TaskEvent.CONSULT,
564
+ destination: 'agent-2',
565
+ destinationType: 'agent',
566
+ });
567
+ // Consult requested but consultee (agent-2) has not answered yet.
568
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
569
+
570
+ // Agent 1 ends the consult before agent-2 answers. Backend AgentConsultEnded:
571
+ // main on hold, self consultCompleted, no consult media, consultee not in participants.
572
+ const consultEndedTaskData = createTaskData({
573
+ agentId: 'agent-1',
574
+ mediaResourceId: 'interaction-1',
575
+ consultMediaResourceId: 'consult-media',
576
+ destAgentId: 'agent-2',
577
+ destinationType: 'Agent',
578
+ type: 'AgentConsultEnded' as any,
579
+ isConsulted: false,
580
+ interaction: {
581
+ state: 'connected',
582
+ interactionId: 'interaction-1',
583
+ mainInteractionId: 'interaction-1',
584
+ owner: 'agent-1',
585
+ participants: {
586
+ 'agent-1': {
587
+ id: 'agent-1',
588
+ pType: 'Agent',
589
+ hasLeft: false,
590
+ hasJoined: true,
591
+ consultState: 'consultCompleted',
592
+ isConsulted: false,
593
+ },
594
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
595
+ },
596
+ media: {
597
+ 'interaction-1': {
598
+ mediaResourceId: 'interaction-1',
599
+ mType: 'mainCall',
600
+ isHold: true,
601
+ participants: ['customer-1', 'agent-1'],
602
+ },
603
+ },
604
+ } as any,
605
+ });
606
+
607
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
608
+
609
+ const snapshot = service.getSnapshot();
610
+ expect(snapshot.value).toBe(TaskState.HELD);
611
+ expect(snapshot.context.consultInitiator).toBe(false);
612
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(false);
613
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
614
+ expect(snapshot.context.uiControls.main.consult).toEqual({
615
+ isVisible: true,
616
+ isEnabled: true,
617
+ });
618
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
619
+ isVisible: false,
620
+ isEnabled: false,
621
+ });
622
+ });
623
+
624
+ it('stays HELD and clears consult UI when AgentConsultEnded arrives on HELD state', () => {
625
+ const service = startMachine();
626
+ const heldTaskData = createTaskData({
627
+ agentId: 'agent-1',
628
+ mediaResourceId: 'interaction-1',
629
+ interaction: {
630
+ state: 'hold',
631
+ interactionId: 'interaction-1',
632
+ mainInteractionId: 'interaction-1',
633
+ owner: 'agent-1',
634
+ participants: {
635
+ 'agent-1': {
636
+ id: 'agent-1',
637
+ pType: 'Agent',
638
+ hasLeft: false,
639
+ consultState: 'consulting',
640
+ isConsulted: false,
641
+ },
642
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
643
+ },
644
+ media: {
645
+ 'interaction-1': {
646
+ mediaResourceId: 'interaction-1',
647
+ mType: 'mainCall',
648
+ isHold: true,
649
+ participants: ['customer-1', 'agent-1'],
650
+ },
651
+ },
652
+ } as any,
653
+ });
654
+
655
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: heldTaskData});
656
+ service.send({type: TaskEvent.ASSIGN, taskData: heldTaskData});
657
+ service.send({
658
+ type: TaskEvent.HOLD_INITIATED,
659
+ mediaResourceId: heldTaskData.mediaResourceId,
660
+ });
661
+ service.send({
662
+ type: TaskEvent.HOLD_SUCCESS,
663
+ mediaResourceId: heldTaskData.mediaResourceId,
664
+ taskData: heldTaskData,
665
+ });
666
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
667
+
668
+ const consultEndedTaskData = createTaskData({
669
+ agentId: 'agent-1',
670
+ mediaResourceId: 'interaction-1',
671
+ type: 'AgentConsultEnded' as any,
672
+ isConsulted: false,
673
+ interaction: {
674
+ state: 'hold',
675
+ interactionId: 'interaction-1',
676
+ mainInteractionId: 'interaction-1',
677
+ owner: 'agent-1',
678
+ participants: {
679
+ 'agent-1': {
680
+ id: 'agent-1',
681
+ pType: 'Agent',
682
+ hasLeft: false,
683
+ consultState: 'consultCompleted',
684
+ isConsulted: false,
685
+ },
686
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
687
+ },
688
+ media: {
689
+ 'interaction-1': {
690
+ mediaResourceId: 'interaction-1',
691
+ mType: 'mainCall',
692
+ isHold: true,
693
+ participants: ['customer-1', 'agent-1'],
694
+ },
695
+ },
696
+ } as any,
697
+ });
698
+
699
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
700
+
701
+ const snapshot = service.getSnapshot();
702
+ expect(snapshot.value).toBe(TaskState.HELD);
703
+ expect(snapshot.context.consultInitiator).toBe(false);
704
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
705
+ expect(snapshot.context.uiControls.main.hold).toEqual({
706
+ isVisible: true,
707
+ isEnabled: true,
708
+ });
709
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
710
+ isVisible: false,
711
+ isEnabled: false,
712
+ });
713
+ });
714
+
715
+ it('keeps main.consult enabled on HELD after AgentConsultFailed then AgentConsultEnded (RONA)', () => {
716
+ const service = startMachine();
717
+ const heldTaskData = createTaskData({
718
+ agentId: 'agent-1',
719
+ mediaResourceId: 'interaction-1',
720
+ type: 'AgentContactHeld' as any,
721
+ interaction: {
722
+ state: 'hold',
723
+ interactionId: 'interaction-1',
724
+ mainInteractionId: 'interaction-1',
725
+ owner: 'agent-1',
726
+ participants: {
727
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
728
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
729
+ } as any,
730
+ media: {
731
+ 'interaction-1': {
732
+ mediaResourceId: 'interaction-1',
733
+ mType: 'mainCall',
734
+ isHold: true,
735
+ participants: ['customer-1', 'agent-1'],
736
+ },
737
+ } as any,
738
+ } as any,
739
+ });
740
+
741
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: heldTaskData});
742
+ service.send({type: TaskEvent.ASSIGN, taskData: heldTaskData});
743
+ service.send({
744
+ type: TaskEvent.HOLD_INITIATED,
745
+ mediaResourceId: heldTaskData.mediaResourceId,
746
+ });
747
+ service.send({
748
+ type: TaskEvent.HOLD_SUCCESS,
749
+ mediaResourceId: heldTaskData.mediaResourceId,
750
+ taskData: heldTaskData,
751
+ });
752
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
753
+
754
+ const consultCreatedTaskData = createTaskData({
755
+ agentId: 'agent-1',
756
+ mediaResourceId: 'interaction-1',
757
+ consultMediaResourceId: 'consult-media',
758
+ consultingAgentId: 'agent-1',
759
+ destAgentId: 'agent-2',
760
+ isConsulted: false,
761
+ type: 'AgentConsultCreated' as any,
762
+ interaction: {
763
+ state: 'consult',
764
+ interactionId: 'interaction-1',
765
+ mainInteractionId: 'interaction-1',
766
+ owner: 'agent-1',
767
+ participants: {
768
+ 'agent-1': {
769
+ id: 'agent-1',
770
+ pType: 'Agent',
771
+ hasLeft: false,
772
+ consultState: 'consultInitiated',
773
+ isConsulted: false,
774
+ },
775
+ 'agent-2': {
776
+ id: 'agent-2',
777
+ pType: 'Agent',
778
+ hasLeft: false,
779
+ consultState: 'consultReserved',
780
+ isConsulted: true,
781
+ },
782
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
783
+ } as any,
784
+ media: {
785
+ 'interaction-1': {
786
+ mediaResourceId: 'interaction-1',
787
+ mType: 'mainCall',
788
+ isHold: true,
789
+ participants: ['customer-1', 'agent-1'],
790
+ },
791
+ 'consult-media': {
792
+ mediaResourceId: 'consult-media',
793
+ mType: 'consult',
794
+ participants: ['agent-2', 'agent-1'],
795
+ },
796
+ } as any,
797
+ } as any,
798
+ });
799
+ service.send({type: TaskEvent.CONSULT_CREATED, taskData: consultCreatedTaskData});
800
+
801
+ const consultFailedTaskData = createTaskData({
802
+ agentId: 'agent-1',
803
+ mediaResourceId: 'interaction-1',
804
+ consultMediaResourceId: 'consult-media',
805
+ consultingAgentId: 'agent-1',
806
+ destAgentId: 'agent-2',
807
+ isConsulted: false,
808
+ type: 'AgentConsultFailed' as any,
809
+ interaction: {
810
+ state: 'connected',
811
+ interactionId: 'interaction-1',
812
+ mainInteractionId: 'interaction-1',
813
+ owner: 'agent-1',
814
+ participants: {
815
+ 'agent-1': {
816
+ id: 'agent-1',
817
+ pType: 'Agent',
818
+ hasLeft: false,
819
+ consultState: 'consultCompleted',
820
+ isConsulted: false,
821
+ },
822
+ 'agent-2': {
823
+ id: 'agent-2',
824
+ pType: 'Agent',
825
+ hasLeft: false,
826
+ consultState: 'consultReserved',
827
+ isConsulted: true,
828
+ },
829
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
830
+ } as any,
831
+ media: {
832
+ 'interaction-1': {
833
+ mediaResourceId: 'interaction-1',
834
+ mType: 'mainCall',
835
+ isHold: true,
836
+ participants: ['customer-1', 'agent-1'],
837
+ },
838
+ } as any,
839
+ } as any,
840
+ });
841
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData: consultFailedTaskData});
842
+
843
+ const consultEndedTaskData = createTaskData({
844
+ agentId: 'agent-1',
845
+ mediaResourceId: 'interaction-1',
846
+ consultMediaResourceId: 'consult-media',
847
+ isConsulted: false,
848
+ type: 'AgentConsultEnded' as any,
849
+ interaction: consultFailedTaskData.interaction,
850
+ });
851
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
852
+
853
+ const snapshot = service.getSnapshot();
854
+ expect(snapshot.value).toBe(TaskState.HELD);
855
+ expect(snapshot.context.consultInitiator).toBe(false);
856
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
857
+ expect(snapshot.context.uiControls.main.consult).toEqual({
858
+ isVisible: true,
859
+ isEnabled: true,
860
+ });
861
+ expect(snapshot.context.uiControls.main.hold).toEqual({
862
+ isVisible: true,
863
+ isEnabled: true,
864
+ });
865
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
866
+ isVisible: true,
867
+ isEnabled: true,
868
+ });
869
+ });
870
+
871
+ it('returns to main leg (HELD) and does not clear the task after AgentConsultFailed then AgentConsultEnded while CONSULTING (RONA)', () => {
872
+ const service = startMachine();
873
+ const heldTaskData = createTaskData({
874
+ agentId: 'agent-1',
875
+ mediaResourceId: 'interaction-1',
876
+ type: 'AgentContactHeld' as any,
877
+ interaction: {
878
+ state: 'hold',
879
+ interactionId: 'interaction-1',
880
+ mainInteractionId: 'interaction-1',
881
+ owner: 'agent-1',
882
+ participants: {
883
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
884
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
885
+ } as any,
886
+ media: {
887
+ 'interaction-1': {
888
+ mediaResourceId: 'interaction-1',
889
+ mType: 'mainCall',
890
+ isHold: true,
891
+ participants: ['customer-1', 'agent-1'],
892
+ },
893
+ } as any,
894
+ } as any,
895
+ });
896
+
897
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: heldTaskData});
898
+ service.send({type: TaskEvent.ASSIGN, taskData: heldTaskData});
899
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: heldTaskData.mediaResourceId});
900
+ service.send({
901
+ type: TaskEvent.HOLD_SUCCESS,
902
+ mediaResourceId: heldTaskData.mediaResourceId,
903
+ taskData: heldTaskData,
904
+ });
905
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
906
+
907
+ // Agent 1 initiates a consult and AgentConsulting arrives during ringing, moving the
908
+ // initiator into CONSULTING before the consultee answers.
909
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-2', destinationType: 'agent'});
910
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
911
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
912
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
913
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
914
+
915
+ // Consultee RONAs: AgentConsultFailed arrives while still in CONSULTING. Main stays held.
916
+ const consultFailedTaskData = createTaskData({
917
+ agentId: 'agent-1',
918
+ mediaResourceId: 'interaction-1',
919
+ consultMediaResourceId: 'consult-media',
920
+ consultingAgentId: 'agent-1',
921
+ destAgentId: 'agent-2',
922
+ isConsulted: false,
923
+ type: 'AgentConsultFailed' as any,
924
+ interaction: {
925
+ state: 'consult',
926
+ interactionId: 'interaction-1',
927
+ mainInteractionId: 'interaction-1',
928
+ owner: 'agent-1',
929
+ participants: {
930
+ 'agent-1': {
931
+ id: 'agent-1',
932
+ pType: 'Agent',
933
+ hasLeft: false,
934
+ consultState: 'consultCompleted',
935
+ isConsulted: false,
936
+ },
937
+ 'agent-2': {
938
+ id: 'agent-2',
939
+ pType: 'Agent',
940
+ hasLeft: false,
941
+ hasJoined: false,
942
+ consultState: 'consultReserved',
943
+ isConsulted: true,
944
+ },
945
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
946
+ } as any,
947
+ media: {
948
+ 'interaction-1': {
949
+ mediaResourceId: 'interaction-1',
950
+ mType: 'mainCall',
951
+ isHold: true,
952
+ participants: ['customer-1', 'agent-1'],
953
+ },
954
+ 'consult-media': {
955
+ mediaResourceId: 'consult-media',
956
+ mType: 'consult',
957
+ participants: ['agent-2', 'agent-1'],
958
+ },
959
+ } as any,
960
+ } as any,
961
+ });
962
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData: consultFailedTaskData});
963
+ // The initiator must leave CONSULTING and fall back to the held main leg (not stay in
964
+ // CONSULTING, which would let the trailing AgentConsultEnded terminate the task).
965
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
966
+
967
+ // AgentConsultEnded closes out the consult leg.
968
+ const consultEndedTaskData = createTaskData({
969
+ agentId: 'agent-1',
970
+ mediaResourceId: 'interaction-1',
971
+ consultMediaResourceId: 'consult-media',
972
+ isConsulted: false,
973
+ type: 'AgentConsultEnded' as any,
974
+ interaction: {
975
+ state: 'connected',
976
+ interactionId: 'interaction-1',
977
+ mainInteractionId: 'interaction-1',
978
+ owner: 'agent-1',
979
+ participants: {
980
+ 'agent-1': {
981
+ id: 'agent-1',
982
+ pType: 'Agent',
983
+ hasLeft: false,
984
+ consultState: 'consultCompleted',
985
+ isConsulted: false,
986
+ },
987
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
988
+ } as any,
989
+ media: {
990
+ 'interaction-1': {
991
+ mediaResourceId: 'interaction-1',
992
+ mType: 'mainCall',
993
+ isHold: true,
994
+ participants: ['customer-1', 'agent-1'],
995
+ },
996
+ } as any,
997
+ } as any,
998
+ });
999
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
1000
+
1001
+ const snapshot = service.getSnapshot();
1002
+ // The task must remain on the main leg (HELD) and never reach TERMINATED.
1003
+ expect(snapshot.value).toBe(TaskState.HELD);
1004
+ expect(snapshot.value).not.toBe(TaskState.TERMINATED);
1005
+ expect(snapshot.context.consultInitiator).toBe(false);
1006
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
1007
+ expect(snapshot.context.uiControls.main.consult).toEqual({
1008
+ isVisible: true,
1009
+ isEnabled: true,
1010
+ });
1011
+ });
1012
+
1013
+ it('transitions CONSULT_INITIATING to CONSULTING on CONSULTING_ACTIVE and marks destination joined', () => {
1014
+ const service = startMachine();
1015
+ const baseTaskData = createTaskData();
1016
+ const consultingTaskData = createTaskData({
1017
+ agentId: 'agent-1',
1018
+ isConsulted: false,
1019
+ consultMediaResourceId: 'consult-media',
1020
+ interaction: {
1021
+ state: 'consulting',
1022
+ interactionId: 'interaction-1',
1023
+ mainInteractionId: 'interaction-1',
1024
+ participants: {
1025
+ 'agent-1': {
1026
+ id: 'agent-1',
1027
+ pType: 'Agent',
1028
+ hasLeft: false,
1029
+ consultState: 'consulting',
1030
+ isConsulted: false,
1031
+ },
1032
+ 'agent-2': {
1033
+ id: 'agent-2',
1034
+ pType: 'Agent',
1035
+ hasLeft: false,
1036
+ hasJoined: true,
1037
+ consultState: 'consulting',
1038
+ isConsulted: true,
1039
+ },
1040
+ } as any,
1041
+ media: {
1042
+ 'interaction-1': {mediaResourceId: 'interaction-1', mType: 'mainCall', isHold: true},
1043
+ 'consult-media': {
1044
+ mediaResourceId: 'consult-media',
1045
+ mType: 'consult',
1046
+ participants: ['agent-1', 'agent-2'],
1047
+ },
1048
+ } as any,
1049
+ } as any,
1050
+ });
1051
+
1052
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
1053
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
1054
+ service.send({
1055
+ type: TaskEvent.CONSULT,
1056
+ destination: 'agent-2',
1057
+ destinationType: 'agent',
1058
+ });
1059
+
1060
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1061
+
1062
+ service.send({
1063
+ type: TaskEvent.CONSULTING_ACTIVE,
1064
+ consultDestinationAgentJoined: true,
1065
+ taskData: consultingTaskData,
1066
+ });
1067
+
1068
+ const snapshot = service.getSnapshot();
1069
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
1070
+ expect(snapshot.context.consultInitiator).toBe(true);
1071
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(true);
1072
+ });
1073
+
1074
+ it('keeps consultDestinationAgentJoined false while consultee is only reserved, then sets true on actual join', () => {
1075
+ const service = startMachine();
1076
+ const pendingTaskData = createTaskData({
1077
+ isConsulted: false,
1078
+ interaction: {
1079
+ state: 'conference',
1080
+ mainInteractionId: 'interaction-1',
1081
+ interactionId: 'interaction-1',
1082
+ participants: {
1083
+ 'agent-1': {
1084
+ id: 'agent-1',
1085
+ pType: 'Agent',
1086
+ hasLeft: false,
1087
+ consultState: 'consultInitiated',
1088
+ isConsulted: false,
1089
+ },
1090
+ 'agent-2': {
1091
+ id: 'agent-2',
1092
+ pType: 'Agent',
1093
+ hasLeft: false,
1094
+ hasJoined: false,
1095
+ consultState: 'consultReserved',
1096
+ isConsulted: true,
1097
+ },
1098
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
1099
+ },
1100
+ media: {
1101
+ 'interaction-1': {
1102
+ mediaResourceId: 'interaction-1',
1103
+ mType: 'mainCall',
1104
+ participants: ['agent-1', 'customer-1'],
1105
+ isHold: true,
1106
+ },
1107
+ 'consult-media-1': {
1108
+ mediaResourceId: 'consult-media-1',
1109
+ mType: 'consult',
1110
+ participants: ['agent-1', 'agent-2'],
1111
+ isHold: false,
1112
+ },
1113
+ },
1114
+ } as any,
1115
+ });
1116
+
1117
+ service.send({type: TaskEvent.HYDRATE, taskData: pendingTaskData});
1118
+
1119
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1120
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(false);
1121
+
1122
+ const joinedTaskData = {
1123
+ ...pendingTaskData,
1124
+ interaction: {
1125
+ ...pendingTaskData.interaction,
1126
+ participants: {
1127
+ ...pendingTaskData.interaction?.participants,
1128
+ 'agent-2': {
1129
+ ...pendingTaskData.interaction?.participants?.['agent-2'],
1130
+ hasJoined: true,
1131
+ consultState: 'consulting',
1132
+ },
1133
+ },
1134
+ },
1135
+ } as any;
1136
+
1137
+ service.send({type: TaskEvent.CONTACT_UPDATED, taskData: joinedTaskData});
1138
+
1139
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(true);
1140
+ });
1141
+
1142
+ it('hydrates conference consult context flags for active consult leg controls', () => {
1143
+ const service = startMachine();
1144
+ const taskData = createTaskData({
1145
+ type: 'AgentContactUnheld' as any,
1146
+ isConsulted: false,
1147
+ consultingAgentId: 'agent-1',
1148
+ consultMediaResourceId: 'consult-media-1',
1149
+ interaction: {
1150
+ state: 'conference',
1151
+ mainInteractionId: 'interaction-1',
1152
+ interactionId: 'interaction-1',
1153
+ participants: {
1154
+ 'agent-1': {
1155
+ id: 'agent-1',
1156
+ pType: 'Agent',
1157
+ hasLeft: false,
1158
+ consultState: 'consulting',
1159
+ isConsulted: false,
1160
+ },
1161
+ 'agent-2': {
1162
+ id: 'agent-2',
1163
+ pType: 'Agent',
1164
+ hasLeft: false,
1165
+ hasJoined: true,
1166
+ consultState: 'consulting',
1167
+ isConsulted: true,
1168
+ },
1169
+ 'agent-3': {
1170
+ id: 'agent-3',
1171
+ pType: 'Agent',
1172
+ hasLeft: false,
1173
+ consultState: 'conferencing',
1174
+ isConsulted: false,
1175
+ },
1176
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
1177
+ },
1178
+ media: {
1179
+ 'interaction-1': {
1180
+ mediaResourceId: 'interaction-1',
1181
+ mType: 'mainCall',
1182
+ participants: ['agent-1', 'agent-3', 'customer-1'],
1183
+ isHold: false,
1184
+ },
1185
+ 'consult-media-1': {
1186
+ mediaResourceId: 'consult-media-1',
1187
+ mType: 'consult',
1188
+ participants: ['agent-1', 'agent-2'],
1189
+ isHold: false,
1190
+ },
1191
+ },
1192
+ } as any,
1193
+ });
1194
+
1195
+ service.send({type: TaskEvent.HYDRATE, taskData});
1196
+
1197
+ const snapshot = service.getSnapshot();
1198
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
1199
+ expect(snapshot.context.consultInitiator).toBe(true);
1200
+ expect(snapshot.context.consultFromConference).toBe(true);
1201
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(true);
1202
+ expect(snapshot.context.consultCallHeld).toBe(false);
1203
+ });
1204
+
1205
+ it('returns to connected when consult ends after switching back to the main leg', () => {
1206
+ const service = startMachine();
1207
+ const taskData = createTaskData();
1208
+
1209
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1210
+ service.send({type: TaskEvent.ASSIGN, taskData});
1211
+ service.send({
1212
+ type: TaskEvent.CONSULT,
1213
+ destination: 'agent-42',
1214
+ destinationType: 'agent',
1215
+ });
1216
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
1217
+ service.send({type: TaskEvent.SWITCH_TO_MAIN_CALL});
1218
+
1219
+ expect(service.getSnapshot().context.consultCallHeld).toBe(true);
1220
+
1221
+ service.send({type: TaskEvent.CONSULT_END});
1222
+
1223
+ const snapshotAfterEnd = service.getSnapshot();
1224
+ expect(snapshotAfterEnd.value).toBe(TaskState.CONNECTED);
1225
+ expect(snapshotAfterEnd.context.consultCallHeld).toBe(false);
1226
+ });
1227
+
1228
+ it('downgrades to HELD on CONSULT_END when conference has downgraded and conferenceHoldParticipant is true', () => {
1229
+ const service = startMachine();
1230
+ const conferenceTaskData = createConferenceConsultTaskData({
1231
+ interactionState: 'conference',
1232
+ includeSecondAgent: true,
1233
+ conferenceHoldParticipant: false,
1234
+ });
1235
+ const downgradedHeldTaskData = createConferenceConsultTaskData({
1236
+ interactionState: 'hold',
1237
+ includeSecondAgent: false,
1238
+ conferenceHoldParticipant: true,
1239
+ isMainHeld: true,
1240
+ });
1241
+
1242
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1243
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1244
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1245
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1246
+
1247
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
1248
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1249
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: conferenceTaskData});
1250
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1251
+
1252
+ service.send({type: TaskEvent.CONSULT_END, taskData: downgradedHeldTaskData});
1253
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
1254
+ });
1255
+
1256
+ it('downgrades to CONNECTED on CONSULT_END when conference has downgraded and conferenceHoldParticipant is false', () => {
1257
+ const service = startMachine();
1258
+ const conferenceTaskData = createConferenceConsultTaskData({
1259
+ interactionState: 'conference',
1260
+ includeSecondAgent: true,
1261
+ conferenceHoldParticipant: false,
1262
+ });
1263
+ const downgradedConnectedTaskData = createConferenceConsultTaskData({
1264
+ interactionState: 'connected',
1265
+ includeSecondAgent: false,
1266
+ conferenceHoldParticipant: false,
1267
+ isMainHeld: false,
1268
+ });
1269
+
1270
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1271
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1272
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1273
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1274
+
1275
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
1276
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1277
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: conferenceTaskData});
1278
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1279
+
1280
+ service.send({type: TaskEvent.CONSULT_END, taskData: downgradedConnectedTaskData});
1281
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1282
+ });
1283
+
1284
+ it('returns to CONFERENCING on CONSULT_END when conference is still active', () => {
1285
+ const service = startMachine();
1286
+ const conferenceTaskData = createConferenceConsultTaskData({
1287
+ interactionState: 'conference',
1288
+ includeSecondAgent: true,
1289
+ conferenceHoldParticipant: false,
1290
+ });
1291
+ const stillConferenceTaskData = createConferenceConsultTaskData({
1292
+ interactionState: 'conference',
1293
+ includeSecondAgent: true,
1294
+ conferenceHoldParticipant: false,
1295
+ });
1296
+
1297
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1298
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1299
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1300
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1301
+
1302
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
1303
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1304
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: conferenceTaskData});
1305
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1306
+
1307
+ service.send({type: TaskEvent.CONSULT_END, taskData: stillConferenceTaskData});
1308
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1309
+ });
1310
+
1311
+ it('transitions CONFERENCING to CONSULTING on CONSULTING_ACTIVE and marks DN consult joined', () => {
1312
+ const service = startMachine();
1313
+ const conferenceTaskData = createConferenceConsultTaskData({
1314
+ interactionState: 'conference',
1315
+ includeSecondAgent: true,
1316
+ conferenceHoldParticipant: false,
1317
+ });
1318
+
1319
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1320
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1321
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1322
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1323
+
1324
+ const dnConsultTaskData = createTaskData({
1325
+ type: 'AgentConsulting' as any,
1326
+ agentId: 'agent-1',
1327
+ consultingAgentId: 'agent-1',
1328
+ isConsulted: false,
1329
+ destinationType: 'DN',
1330
+ consultMediaResourceId: 'consult-media-1',
1331
+ interaction: {
1332
+ state: 'conference',
1333
+ mainInteractionId: 'interaction-1',
1334
+ interactionId: 'interaction-1',
1335
+ participants: {
1336
+ 'agent-1': {
1337
+ id: 'agent-1',
1338
+ pType: 'Agent',
1339
+ hasLeft: false,
1340
+ consultState: 'consulting',
1341
+ isConsulted: false,
1342
+ },
1343
+ 'agent-2': {
1344
+ id: 'agent-2',
1345
+ pType: 'Agent',
1346
+ hasLeft: false,
1347
+ consultState: 'conferencing',
1348
+ },
1349
+ 'dn-dest': {
1350
+ id: 'dn-dest',
1351
+ pType: 'DN',
1352
+ hasLeft: false,
1353
+ hasJoined: true,
1354
+ },
1355
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
1356
+ },
1357
+ media: {
1358
+ 'interaction-1': {
1359
+ mediaResourceId: 'interaction-1',
1360
+ mType: 'mainCall',
1361
+ participants: ['agent-1', 'agent-2', 'customer-1'],
1362
+ isHold: false,
1363
+ },
1364
+ 'consult-media-1': {
1365
+ mediaResourceId: 'consult-media-1',
1366
+ mType: 'consult',
1367
+ participants: ['agent-1', 'dn-dest'],
1368
+ isHold: false,
1369
+ },
1370
+ },
1371
+ } as any,
1372
+ });
1373
+
1374
+ service.send({
1375
+ type: TaskEvent.CONSULTING_ACTIVE,
1376
+ consultDestinationAgentJoined: true,
1377
+ taskData: dnConsultTaskData,
1378
+ });
1379
+
1380
+ const snapshot = service.getSnapshot();
1381
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
1382
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(true);
1383
+ expect(snapshot.context.consultFromConference).toBe(true);
1384
+ expect(snapshot.context.consultDestinationType).toBe('entryPoint');
1385
+ });
1386
+
1387
+ it('transitions to conferencing when merge event is received', () => {
1388
+ const service = startMachine();
1389
+ const taskData = createTaskData({consultingAgentId: 'agent-1'});
1390
+
1391
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1392
+ service.send({type: TaskEvent.ASSIGN, taskData});
1393
+ service.send({
1394
+ type: TaskEvent.CONSULT,
1395
+ destination: 'agent-42',
1396
+ destinationType: 'agent',
1397
+ });
1398
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1399
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1400
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1401
+
1402
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1403
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
1404
+
1405
+ service.send({type: TaskEvent.CONFERENCE_START});
1406
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1407
+ });
1408
+
1409
+ it('sets hideBlindTransfer flag and clears consult context on EP-DN AgentConsultConferencing (CAI-8329)', () => {
1410
+ const service = startMachine();
1411
+ const agentId = 'agent-1';
1412
+ const consultMediaId = 'consult-media-1';
1413
+ const consultTaskData = createTaskData({
1414
+ agentId,
1415
+ consultingAgentId: agentId,
1416
+ consultMediaResourceId: consultMediaId,
1417
+ isConsulted: false,
1418
+ interaction: {
1419
+ state: 'consulting',
1420
+ interactionId: 'interaction-1',
1421
+ mainInteractionId: 'interaction-1',
1422
+ owner: agentId,
1423
+ participants: {
1424
+ 'agent-1': {
1425
+ id: 'agent-1',
1426
+ pType: 'Agent',
1427
+ hasJoined: true,
1428
+ hasLeft: false,
1429
+ consultState: 'consulting',
1430
+ isConsulted: false,
1431
+ },
1432
+ '+13159998087': {
1433
+ id: '+13159998087',
1434
+ pType: 'EP-DN',
1435
+ hasLeft: false,
1436
+ },
1437
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
1438
+ },
1439
+ media: {
1440
+ 'interaction-1': {
1441
+ mediaResourceId: 'interaction-1',
1442
+ mType: 'mainCall',
1443
+ participants: ['customer-1', 'agent-1'],
1444
+ isHold: false,
1445
+ },
1446
+ [consultMediaId]: {
1447
+ mediaResourceId: consultMediaId,
1448
+ mType: 'consult',
1449
+ participants: ['+13159998087', 'agent-1'],
1450
+ isHold: false,
1451
+ },
1452
+ },
1453
+ } as any,
1454
+ });
1455
+
1456
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: consultTaskData});
1457
+ service.send({type: TaskEvent.ASSIGN, taskData: consultTaskData});
1458
+ service.send({
1459
+ type: TaskEvent.CONSULT,
1460
+ destination: '+13159998087',
1461
+ destinationType: 'entryPoint',
1462
+ });
1463
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: consultTaskData});
1464
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1465
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
1466
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
1467
+
1468
+ const conferencingTaskData = createTaskData({
1469
+ agentId,
1470
+ consultMediaResourceId: consultMediaId,
1471
+ isConsulted: false,
1472
+ type: 'AgentConsultConferencing' as any,
1473
+ interaction: {
1474
+ state: 'conference',
1475
+ interactionId: 'interaction-1',
1476
+ mainInteractionId: 'interaction-1',
1477
+ owner: agentId,
1478
+ participants: {
1479
+ 'agent-1': {
1480
+ id: 'agent-1',
1481
+ pType: 'Agent',
1482
+ hasJoined: true,
1483
+ hasLeft: false,
1484
+ consultState: 'conferencing',
1485
+ isConsulted: false,
1486
+ },
1487
+ '+13159998087': {
1488
+ id: '+13159998087',
1489
+ pType: 'EP-DN',
1490
+ hasLeft: false,
1491
+ },
1492
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
1493
+ },
1494
+ media: {
1495
+ 'interaction-1': {
1496
+ mediaResourceId: 'interaction-1',
1497
+ mType: 'mainCall',
1498
+ participants: ['customer-1', 'agent-1'],
1499
+ isHold: false,
1500
+ },
1501
+ [consultMediaId]: {
1502
+ mediaResourceId: consultMediaId,
1503
+ mType: 'consult',
1504
+ participants: ['+13159998087', 'agent-1'],
1505
+ isHold: false,
1506
+ },
1507
+ },
1508
+ } as any,
1509
+ });
1510
+
1511
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferencingTaskData});
1512
+
1513
+ const snapshot = service.getSnapshot();
1514
+ expect(snapshot.value).toBe(TaskState.CONFERENCING);
1515
+ expect(snapshot.context.consultInitiator).toBe(false);
1516
+ expect(snapshot.context.consultDestinationType).toBe(null);
1517
+ expect(snapshot.context.hideBlindTransferForEpDnPendingMerge).toBe(true);
1518
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
1519
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
1520
+ isVisible: false,
1521
+ isEnabled: false,
1522
+ });
1523
+ expect(snapshot.context.uiControls.main.exitConference).toEqual({
1524
+ isVisible: true,
1525
+ isEnabled: true,
1526
+ });
1527
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
1528
+ isVisible: false,
1529
+ isEnabled: false,
1530
+ });
1531
+ });
1532
+
1533
+ it('terminates via wrapup when EXIT_CONFERENCE_SUCCESS is received in conferencing', () => {
1534
+ const service = startMachine();
1535
+ const taskData = createTaskData({
1536
+ // shouldWrapUpForThisAgent will return true when owner matches self agent
1537
+ interaction: {
1538
+ owner: 'agent-1',
1539
+ state: 'conference',
1540
+ mainInteractionId: 'interaction-1',
1541
+ interactionId: 'interaction-1',
1542
+ participants: {
1543
+ 'agent-1': {
1544
+ id: 'agent-1',
1545
+ pType: 'Agent',
1546
+ type: 'Agent',
1547
+ hasJoined: true,
1548
+ hasLeft: false,
1549
+ isInPredial: false,
1550
+ },
1551
+ c1: {
1552
+ id: 'c1',
1553
+ pType: 'Customer',
1554
+ type: 'Customer',
1555
+ hasJoined: true,
1556
+ hasLeft: false,
1557
+ isInPredial: false,
1558
+ },
1559
+ },
1560
+ media: {
1561
+ 'interaction-1': {
1562
+ mediaResourceId: 'interaction-1',
1563
+ mediaType: 'telephony',
1564
+ mediaMgr: 'mm',
1565
+ participants: ['agent-1', 'c1'],
1566
+ mType: 'mainCall',
1567
+ isHold: false,
1568
+ holdTimestamp: null,
1569
+ },
1570
+ },
1571
+ } as any,
1572
+ });
1573
+
1574
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1575
+ service.send({type: TaskEvent.ASSIGN, taskData});
1576
+ service.send({
1577
+ type: TaskEvent.CONSULT,
1578
+ destination: 'agent-42',
1579
+ destinationType: 'agent',
1580
+ });
1581
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1582
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1583
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
1584
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1585
+
1586
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
1587
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
1588
+ });
1589
+
1590
+ it('terminates when EXIT_CONFERENCE_SUCCESS is received in conferencing and wrapup is not required', () => {
1591
+ const service = startMachine();
1592
+ const taskData = createTaskData({
1593
+ isConsulted: true,
1594
+ wrapUpRequired: false,
1595
+ interaction: {
1596
+ owner: 'other-agent',
1597
+ state: 'conference',
1598
+ mainInteractionId: 'interaction-1',
1599
+ interactionId: 'interaction-1',
1600
+ participants: {
1601
+ 'agent-1': {
1602
+ id: 'agent-1',
1603
+ pType: 'Agent',
1604
+ type: 'Agent',
1605
+ hasJoined: true,
1606
+ hasLeft: false,
1607
+ isInPredial: false,
1608
+ isWrapUp: false,
1609
+ },
1610
+ c1: {
1611
+ id: 'c1',
1612
+ pType: 'Customer',
1613
+ type: 'Customer',
1614
+ hasJoined: true,
1615
+ hasLeft: false,
1616
+ isInPredial: false,
1617
+ },
1618
+ },
1619
+ media: {
1620
+ 'interaction-1': {
1621
+ mediaResourceId: 'interaction-1',
1622
+ mediaType: 'telephony',
1623
+ mediaMgr: 'mm',
1624
+ participants: ['agent-1', 'c1'],
1625
+ mType: 'mainCall',
1626
+ isHold: false,
1627
+ holdTimestamp: null,
1628
+ },
1629
+ },
1630
+ } as any,
1631
+ });
1632
+
1633
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1634
+ service.send({type: TaskEvent.ASSIGN, taskData});
1635
+ service.send({
1636
+ type: TaskEvent.CONSULT,
1637
+ destination: 'agent-42',
1638
+ destinationType: 'agent',
1639
+ });
1640
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1641
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1642
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
1643
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1644
+
1645
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
1646
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
1647
+ });
1648
+
1649
+ it('downgrades to HELD on HOLD_SUCCESS when no other agents remain on task', () => {
1650
+ const service = startMachine();
1651
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
1652
+ const heldTaskData = createSingleAgentConferenceTaskData('hold', true);
1653
+
1654
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1655
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1656
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1657
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1658
+
1659
+ service.send({
1660
+ type: TaskEvent.HOLD_SUCCESS,
1661
+ mediaResourceId: heldTaskData.mediaResourceId,
1662
+ taskData: heldTaskData,
1663
+ });
1664
+
1665
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
1666
+ });
1667
+
1668
+ it('downgrades to CONNECTED on UNHOLD_SUCCESS when no other agents remain on task', () => {
1669
+ const service = startMachine();
1670
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
1671
+ const connectedTaskData = createSingleAgentConferenceTaskData('connected', false);
1672
+
1673
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1674
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1675
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1676
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1677
+
1678
+ service.send({
1679
+ type: TaskEvent.UNHOLD_SUCCESS,
1680
+ mediaResourceId: connectedTaskData.mediaResourceId,
1681
+ taskData: connectedTaskData,
1682
+ });
1683
+
1684
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1685
+ });
1686
+
1687
+ it('returns to CONNECTED when CTQ cancel arrives before queue connects', () => {
1688
+ const service = startMachine();
1689
+ const taskData = createTaskData();
1690
+
1691
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1692
+ service.send({type: TaskEvent.ASSIGN, taskData});
1693
+
1694
+ service.send({
1695
+ type: TaskEvent.CONSULT,
1696
+ destination: 'queue-1',
1697
+ destinationType: 'queue',
1698
+ });
1699
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1700
+
1701
+ service.send({type: TaskEvent.CTQ_CANCEL});
1702
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1703
+ });
1704
+
1705
+ it('returns to CONNECTED when CTQ consult fails', () => {
1706
+ const service = startMachine();
1707
+ const taskData = createTaskData();
1708
+
1709
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1710
+ service.send({type: TaskEvent.ASSIGN, taskData});
1711
+
1712
+ service.send({
1713
+ type: TaskEvent.CONSULT,
1714
+ destination: 'queue-1',
1715
+ destinationType: 'queue',
1716
+ });
1717
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1718
+
1719
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData});
1720
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1721
+ });
1722
+ });
1723
+
1724
+ describe('failure scenarios', () => {
1725
+ it('returns to CONNECTED when HOLD fails', () => {
1726
+ const service = startMachine();
1727
+ const taskData = createTaskData();
1728
+
1729
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1730
+ service.send({type: TaskEvent.ASSIGN, taskData});
1731
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
1732
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
1733
+
1734
+ service.send({
1735
+ type: TaskEvent.HOLD_FAILED,
1736
+ mediaResourceId: taskData.mediaResourceId,
1737
+ });
1738
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1739
+ });
1740
+
1741
+ it('falls back to HELD when UNHOLD fails', () => {
1742
+ const service = startMachine();
1743
+ const taskData = createTaskData();
1744
+
1745
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1746
+ service.send({type: TaskEvent.ASSIGN, taskData});
1747
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
1748
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
1749
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
1750
+
1751
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
1752
+ expect(service.getSnapshot().value).toBe(TaskState.RESUME_INITIATING);
1753
+
1754
+ service.send({type: TaskEvent.UNHOLD_FAILED, mediaResourceId: taskData.mediaResourceId});
1755
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
1756
+ });
1757
+ });
1758
+
1759
+ describe('CONF_INITIATING state event handlers', () => {
1760
+ it('transitions to CONFERENCING on CONFERENCE_START', () => {
1761
+ const service = startMachine();
1762
+ const taskData = createTaskData({consultingAgentId: 'agent-1'});
1763
+
1764
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1765
+ service.send({type: TaskEvent.ASSIGN, taskData});
1766
+ service.send({
1767
+ type: TaskEvent.CONSULT,
1768
+ destination: 'agent-42',
1769
+ destinationType: 'agent',
1770
+ });
1771
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1772
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1773
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
1774
+
1775
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
1776
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1777
+ });
1778
+
1779
+ it('transitions to WRAPPING_UP on CONSULT_END with isTerminated during CONF_INITIATING', () => {
1780
+ const service = startMachine();
1781
+ const taskData = createTaskData({consultingAgentId: 'agent-1'});
1782
+
1783
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1784
+ service.send({type: TaskEvent.ASSIGN, taskData});
1785
+ service.send({
1786
+ type: TaskEvent.CONSULT,
1787
+ destination: 'agent-42',
1788
+ destinationType: 'agent',
1789
+ });
1790
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1791
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1792
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
1793
+
1794
+ const terminatedTaskData = createTaskData({
1795
+ consultingAgentId: 'agent-1',
1796
+ interaction: {
1797
+ isTerminated: true,
1798
+ owner: 'agent-1',
1799
+ } as any,
1800
+ });
1801
+ service.send({type: TaskEvent.CONSULT_END, taskData: terminatedTaskData});
1802
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
1803
+ });
1804
+
1805
+ it('transitions to CONNECTED on CONSULT_END without isTerminated during CONF_INITIATING', () => {
1806
+ const service = startMachine();
1807
+ const taskData = createTaskData({consultingAgentId: 'agent-1'});
1808
+
1809
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1810
+ service.send({type: TaskEvent.ASSIGN, taskData});
1811
+ service.send({
1812
+ type: TaskEvent.CONSULT,
1813
+ destination: 'agent-42',
1814
+ destinationType: 'agent',
1815
+ });
1816
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1817
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1818
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
1819
+
1820
+ service.send({type: TaskEvent.CONSULT_END, taskData});
1821
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1822
+ });
1823
+
1824
+ });
1825
+
1826
+
1827
+ describe('CONFERENCING state CONSULT_FAILED ANOTHER_CONSULT_IN_PROGRESS (CAI-8329)', () => {
1828
+ it('keeps hideBlindTransfer flag and hides transfer after secondary consult rejected from Agent Desktop', () => {
1829
+ const service = startMachine();
1830
+ const agentId = 'agent-1';
1831
+ const consultMediaId = 'consult-media-1';
1832
+ const baseTaskData = createTaskData({
1833
+ agentId,
1834
+ consultingAgentId: agentId,
1835
+ consultMediaResourceId: consultMediaId,
1836
+ isConsulted: false,
1837
+ interaction: {
1838
+ state: 'conference',
1839
+ interactionId: 'interaction-1',
1840
+ mainInteractionId: 'interaction-1',
1841
+ owner: agentId,
1842
+ participants: {
1843
+ [agentId]: {
1844
+ id: agentId,
1845
+ pType: 'Agent',
1846
+ hasJoined: true,
1847
+ hasLeft: false,
1848
+ consultState: 'conferencing',
1849
+ },
1850
+ '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
1851
+ '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: false},
1852
+ },
1853
+ media: {
1854
+ 'interaction-1': {
1855
+ mediaResourceId: 'interaction-1',
1856
+ mType: 'mainCall',
1857
+ participants: ['+14696762938', agentId],
1858
+ isHold: false,
1859
+ },
1860
+ [consultMediaId]: {
1861
+ mediaResourceId: consultMediaId,
1862
+ mType: 'consult',
1863
+ participants: ['+13159998059', agentId],
1864
+ isHold: false,
1865
+ },
1866
+ },
1867
+ } as any,
1868
+ });
1869
+
1870
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
1871
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
1872
+ service.send({
1873
+ type: TaskEvent.CONSULT,
1874
+ destination: '+13159998059',
1875
+ destinationType: 'entryPoint',
1876
+ });
1877
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: baseTaskData});
1878
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1879
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: baseTaskData});
1880
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1881
+ expect(service.getSnapshot().context.hideBlindTransferForEpDnPendingMerge).toBe(true);
1882
+
1883
+ const consultFailedTaskData = createTaskData({
1884
+ agentId,
1885
+ consultingAgentId: agentId,
1886
+ consultMediaResourceId: consultMediaId,
1887
+ isConsulted: false,
1888
+ destinationType: 'Agent',
1889
+ reason: 'ANOTHER_CONSULT_IN_PROGRESS',
1890
+ type: 'AgentConsultFailed' as any,
1891
+ interaction: {
1892
+ state: 'conference',
1893
+ interactionId: 'interaction-1',
1894
+ mainInteractionId: 'interaction-1',
1895
+ owner: agentId,
1896
+ participants: {
1897
+ [agentId]: {
1898
+ id: agentId,
1899
+ pType: 'Agent',
1900
+ hasJoined: true,
1901
+ hasLeft: false,
1902
+ consultState: 'conferencing',
1903
+ },
1904
+ '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
1905
+ '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: false},
1906
+ },
1907
+ media: {
1908
+ 'interaction-1': {
1909
+ mediaResourceId: 'interaction-1',
1910
+ mType: 'mainCall',
1911
+ participants: ['+14696762938', agentId],
1912
+ isHold: false,
1913
+ },
1914
+ [consultMediaId]: {
1915
+ mediaResourceId: consultMediaId,
1916
+ mType: 'consult',
1917
+ participants: ['+13159998059', agentId],
1918
+ isHold: false,
1919
+ },
1920
+ },
1921
+ } as any,
1922
+ });
1923
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData: consultFailedTaskData, reason: 'ANOTHER_CONSULT_IN_PROGRESS'});
1924
+
1925
+ const snapshot = service.getSnapshot();
1926
+ expect(snapshot.value).toBe(TaskState.CONFERENCING);
1927
+ expect(snapshot.context.hideBlindTransferForEpDnPendingMerge).toBe(true);
1928
+ expect(snapshot.context.uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
1929
+ expect(snapshot.context.uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
1930
+ });
1931
+ });
1932
+
1933
+ describe('CONFERENCING state TASK_WRAPUP after EP-DN post-call (CAI-8329)', () => {
1934
+ it('transitions to WRAPPING_UP on AgentWrapup while in CONFERENCING post_call', () => {
1935
+ const service = startMachine();
1936
+ const agentId = 'agent-1';
1937
+ const consultMediaId = 'consult-media-1';
1938
+ const baseTaskData = createTaskData({
1939
+ agentId,
1940
+ consultingAgentId: agentId,
1941
+ consultMediaResourceId: consultMediaId,
1942
+ isConsulted: false,
1943
+ interaction: {
1944
+ state: 'conference',
1945
+ interactionId: 'interaction-1',
1946
+ mainInteractionId: 'interaction-1',
1947
+ owner: agentId,
1948
+ participants: {
1949
+ [agentId]: {
1950
+ id: agentId,
1951
+ pType: 'Agent',
1952
+ hasJoined: true,
1953
+ hasLeft: false,
1954
+ consultState: 'conferencing',
1955
+ },
1956
+ '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
1957
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
1958
+ },
1959
+ media: {
1960
+ 'interaction-1': {
1961
+ mediaResourceId: 'interaction-1',
1962
+ mType: 'mainCall',
1963
+ participants: ['customer-1', agentId],
1964
+ isHold: false,
1965
+ },
1966
+ [consultMediaId]: {
1967
+ mediaResourceId: consultMediaId,
1968
+ mType: 'consult',
1969
+ participants: ['+13159998059', agentId],
1970
+ isHold: false,
1971
+ },
1972
+ },
1973
+ } as any,
1974
+ });
1975
+
1976
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
1977
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
1978
+ service.send({
1979
+ type: TaskEvent.CONSULT,
1980
+ destination: '+13159998059',
1981
+ destinationType: 'entryPoint',
1982
+ });
1983
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: baseTaskData});
1984
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1985
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: baseTaskData});
1986
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1987
+
1988
+ const postCallTaskData = createTaskData({
1989
+ agentId,
1990
+ consultMediaResourceId: consultMediaId,
1991
+ isConsulted: false,
1992
+ type: 'ParticipantPostCallActivity' as any,
1993
+ interaction: {
1994
+ state: 'post_call',
1995
+ interactionId: 'interaction-1',
1996
+ mainInteractionId: 'interaction-1',
1997
+ owner: agentId,
1998
+ isTerminated: false,
1999
+ callProcessingDetails: {hasCustomerLeft: 'true'},
2000
+ participants: {
2001
+ [agentId]: {
2002
+ id: agentId,
2003
+ pType: 'Agent',
2004
+ hasJoined: true,
2005
+ hasLeft: false,
2006
+ consultState: 'conferencing',
2007
+ currentState: 'post_call',
2008
+ },
2009
+ '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
2010
+ '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: true},
2011
+ },
2012
+ media: {
2013
+ 'interaction-1': {
2014
+ mediaResourceId: 'interaction-1',
2015
+ mType: 'mainCall',
2016
+ participants: [agentId],
2017
+ isHold: false,
2018
+ },
2019
+ [consultMediaId]: {
2020
+ mediaResourceId: consultMediaId,
2021
+ mType: 'consult',
2022
+ participants: ['+13159998059', agentId],
2023
+ isHold: false,
2024
+ },
2025
+ },
2026
+ } as any,
2027
+ });
2028
+ service.send({type: TaskEvent.PARTICIPANT_LEAVE, taskData: postCallTaskData});
2029
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2030
+ expect(service.getSnapshot().context.hideBlindTransferForEpDnPendingMerge).toBe(true);
2031
+
2032
+ const wrapupTaskData = createTaskData({
2033
+ agentId,
2034
+ wrapUpRequired: true,
2035
+ type: 'AgentWrapup' as any,
2036
+ interaction: {
2037
+ state: 'post_call',
2038
+ interactionId: 'interaction-1',
2039
+ mainInteractionId: 'interaction-1',
2040
+ owner: agentId,
2041
+ isTerminated: true,
2042
+ participants: {
2043
+ [agentId]: {
2044
+ id: agentId,
2045
+ pType: 'Agent',
2046
+ hasJoined: true,
2047
+ hasLeft: false,
2048
+ consultState: 'consultCompleted',
2049
+ isWrapUp: true,
2050
+ },
2051
+ '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: true},
2052
+ },
2053
+ media: {
2054
+ 'interaction-1': {
2055
+ mediaResourceId: 'interaction-1',
2056
+ mType: 'mainCall',
2057
+ participants: [],
2058
+ isHold: false,
2059
+ },
2060
+ },
2061
+ } as any,
2062
+ });
2063
+ service.send({type: TaskEvent.TASK_WRAPUP, taskData: wrapupTaskData});
2064
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2065
+ });
2066
+ });
2067
+
2068
+ describe('CONFERENCING state CONSULT_END with terminated interaction', () => {
2069
+ it('transitions to WRAPPING_UP when CONSULT_END arrives with isTerminated in CONFERENCING', () => {
2070
+ const service = startMachine();
2071
+ const taskData = createTaskData({
2072
+ consultingAgentId: 'agent-1',
2073
+ interaction: {
2074
+ owner: 'agent-1',
2075
+ state: 'conference',
2076
+ } as any,
2077
+ });
2078
+
2079
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2080
+ service.send({type: TaskEvent.ASSIGN, taskData});
2081
+ service.send({
2082
+ type: TaskEvent.CONSULT,
2083
+ destination: 'agent-42',
2084
+ destinationType: 'agent',
2085
+ });
2086
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2087
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2088
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
2089
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2090
+
2091
+ const terminatedTaskData = createTaskData({
2092
+ consultingAgentId: 'agent-1',
2093
+ interaction: {
2094
+ isTerminated: true,
2095
+ owner: 'agent-1',
2096
+ state: 'conference',
2097
+ } as any,
2098
+ });
2099
+ service.send({type: TaskEvent.CONSULT_END, taskData: terminatedTaskData});
2100
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2101
+ });
2102
+ });
2103
+
2104
+ describe('OFFERED state event handlers', () => {
2105
+ it('transitions to TERMINATED when customer disconnects before agent answers', () => {
2106
+ const service = startMachine();
2107
+ const taskData = createTaskData({isConsulted: false});
2108
+
2109
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2110
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2111
+
2112
+ service.send({type: TaskEvent.CONTACT_ENDED, taskData});
2113
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2114
+ });
2115
+
2116
+ it('transitions to TERMINATED when consulted agent does not answer', () => {
2117
+ const service = startMachine();
2118
+ const taskData = createTaskData({
2119
+ isConsulted: true,
2120
+ consultingAgentId: 'agent-1',
2121
+ });
2122
+
2123
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2124
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2125
+
2126
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData});
2127
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2128
+ });
2129
+ });
2130
+
2131
+ describe('OUTBOUND_FAILED handling', () => {
2132
+ it('transitions from IDLE to TERMINATED on OUTBOUND_FAILED (race condition)', () => {
2133
+ const service = startMachine();
2134
+ expect(service.getSnapshot().value).toBe(TaskState.IDLE);
2135
+
2136
+ const taskData = createTaskData({
2137
+ interaction: {
2138
+ outboundType: 'OUTDIAL',
2139
+ isTerminated: true,
2140
+ } as any,
2141
+ });
2142
+
2143
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData, reason: 'CUSTOMER_BUSY'});
2144
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2145
+ });
2146
+
2147
+ it('transitions from OFFERED to TERMINATED on OUTBOUND_FAILED without wrapup', () => {
2148
+ const service = startMachine();
2149
+ const offerTaskData = createTaskData({
2150
+ interaction: {
2151
+ outboundType: 'OUTDIAL',
2152
+ } as any,
2153
+ });
2154
+
2155
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2156
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2157
+
2158
+ const failedTaskData = createTaskData({
2159
+ interaction: {
2160
+ outboundType: 'OUTDIAL',
2161
+ isTerminated: true,
2162
+ } as any,
2163
+ });
2164
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
2165
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2166
+ });
2167
+
2168
+ it('transitions from OFFERED to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2169
+ const service = startMachine();
2170
+ const offerTaskData = createTaskData({
2171
+ interaction: {
2172
+ outboundType: 'OUTDIAL',
2173
+ } as any,
2174
+ });
2175
+
2176
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2177
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2178
+
2179
+ const failedTaskData = createTaskData({
2180
+ agentId: 'agent-1',
2181
+ agentsPendingWrapUp: ['agent-1'],
2182
+ interaction: {
2183
+ outboundType: 'OUTDIAL',
2184
+ isTerminated: true,
2185
+ } as any,
2186
+ });
2187
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
2188
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2189
+ });
2190
+ });
2191
+
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();
2198
+
2199
+ return actor;
2200
+ };
2201
+
2202
+ const createSecondaryAgentConferenceBaseTaskData = () =>
2203
+ createTaskData({
2204
+ agentId: 'agent-2',
2205
+ isConsulted: true,
2206
+ interactionId: 'interaction-1',
2207
+ mediaResourceId: 'interaction-1',
2208
+ consultMediaResourceId: 'consult-media-1',
2209
+ interaction: {
2210
+ state: 'conference',
2211
+ owner: 'agent-1',
2212
+ mainInteractionId: 'interaction-1',
2213
+ interactionId: 'interaction-1',
2214
+ participants: {
2215
+ 'agent-1': {
2216
+ id: 'agent-1',
2217
+ pType: 'Agent',
2218
+ hasLeft: false,
2219
+ consultState: 'conferencing',
2220
+ },
2221
+ 'agent-2': {
2222
+ id: 'agent-2',
2223
+ pType: 'Agent',
2224
+ hasLeft: false,
2225
+ isConsulted: true,
2226
+ consultState: 'conferencing',
2227
+ },
2228
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2229
+ },
2230
+ media: {
2231
+ 'interaction-1': {
2232
+ mediaResourceId: 'interaction-1',
2233
+ mType: 'mainCall',
2234
+ participants: ['agent-1', 'agent-2', 'customer-1'],
2235
+ isHold: false,
2236
+ },
2237
+ },
2238
+ } as any,
2239
+ });
2240
+
2241
+ const createSecondaryAgentConsultingTaskData = () =>
2242
+ createTaskData({
2243
+ agentId: 'agent-2',
2244
+ isConsulted: true,
2245
+ consultingAgentId: 'agent-2',
2246
+ type: 'AgentConferenceTransferred' as any,
2247
+ interactionId: 'interaction-1',
2248
+ mediaResourceId: 'interaction-1',
2249
+ consultMediaResourceId: 'consult-media-1',
2250
+ interaction: {
2251
+ state: 'conference',
2252
+ owner: 'agent-1',
2253
+ mainInteractionId: 'interaction-1',
2254
+ interactionId: 'interaction-1',
2255
+ participants: {
2256
+ 'agent-1': {
2257
+ id: 'agent-1',
2258
+ pType: 'Agent',
2259
+ hasLeft: false,
2260
+ consultState: 'conferencing',
2261
+ },
2262
+ 'agent-2': {
2263
+ id: 'agent-2',
2264
+ pType: 'Agent',
2265
+ hasLeft: false,
2266
+ isConsulted: true,
2267
+ consultState: 'consulting',
2268
+ },
2269
+ 'agent-3': {
2270
+ id: 'agent-3',
2271
+ pType: 'Agent',
2272
+ hasLeft: false,
2273
+ isConsulted: true,
2274
+ consultState: 'consulting',
2275
+ },
2276
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2277
+ },
2278
+ media: {
2279
+ 'interaction-1': {
2280
+ mediaResourceId: 'interaction-1',
2281
+ mType: 'mainCall',
2282
+ participants: ['agent-1', 'agent-2', 'customer-1'],
2283
+ isHold: false,
2284
+ },
2285
+ 'consult-media-1': {
2286
+ mediaResourceId: 'consult-media-1',
2287
+ mType: 'consult',
2288
+ participants: ['agent-2', 'agent-3'],
2289
+ isHold: false,
2290
+ },
2291
+ },
2292
+ } as any,
2293
+ });
2294
+
2295
+ const setupSecondaryAgentConsultingFromConference = (service: ReturnType<typeof startAgent2Machine>) => {
2296
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
2297
+ const consultingTaskData = createSecondaryAgentConsultingTaskData();
2298
+
2299
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2300
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2301
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2302
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2303
+
2304
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
2305
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
2306
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: consultingTaskData});
2307
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
2308
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
2309
+ expect(service.getSnapshot().context.consultFromConference).toBe(true);
2310
+ };
2311
+
2312
+ const setupSecondaryAgentDesktopConsultingFromConference = (
2313
+ service: ReturnType<typeof startAgent2Machine>
2314
+ ) => {
2315
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
2316
+ const consultingTaskData = createSecondaryAgentConsultingTaskData();
2317
+ const consultingActivePayload = {
2318
+ ...consultingTaskData,
2319
+ type: 'AgentConsulting' as any,
2320
+ };
2321
+ delete (consultingActivePayload as {consultingAgentId?: string}).consultingAgentId;
2322
+
2323
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2324
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2325
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2326
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2327
+
2328
+ service.send({
2329
+ type: TaskEvent.CONSULTING_ACTIVE,
2330
+ consultDestinationAgentJoined: true,
2331
+ taskData: consultingActivePayload,
2332
+ });
2333
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
2334
+ expect(service.getSnapshot().context.consultFromConference).toBe(true);
2335
+ };
2336
+
2337
+ it('terminates secondary agent on true desktop path without widgets CONSULT event', () => {
2338
+ const service = startAgent2Machine();
2339
+ setupSecondaryAgentDesktopConsultingFromConference(service);
2340
+
2341
+ const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
2342
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
2343
+
2344
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2345
+ });
2346
+
2347
+ it('terminates secondary agent on widgets-initiated consult conference transfer from CONSULTING', () => {
2348
+ const service = startAgent2Machine();
2349
+ setupSecondaryAgentConsultingFromConference(service);
2350
+
2351
+ const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
2352
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
2353
+
2354
+ const snapshot = service.getSnapshot();
2355
+ expect(snapshot.value).toBe(TaskState.TERMINATED);
2356
+ expect(snapshot.context.consultInitiator).toBe(false);
2357
+ expect(snapshot.context.consultFromConference).toBe(false);
2358
+ });
2359
+
2360
+ it('moves consultee to CONFERENCING when primary agent conference-transfers', () => {
2361
+ const startAgent3Machine = () => {
2362
+ const actor = createActor(
2363
+ createTaskStateMachine({...createConfig(), agentId: 'agent-3'})
2364
+ );
2365
+ actor.start();
2366
+
2367
+ return actor;
2368
+ };
2369
+
2370
+ const conferenceTaskData = createTaskData({
2371
+ agentId: 'agent-3',
2372
+ isConsulted: true,
2373
+ interactionId: 'interaction-1',
2374
+ mediaResourceId: 'interaction-1',
2375
+ interaction: {
2376
+ state: 'conference',
2377
+ owner: 'agent-1',
2378
+ mainInteractionId: 'interaction-1',
2379
+ interactionId: 'interaction-1',
2380
+ participants: {
2381
+ 'agent-1': {
2382
+ id: 'agent-1',
2383
+ pType: 'Agent',
2384
+ hasLeft: false,
2385
+ consultState: 'conferencing',
2386
+ },
2387
+ 'agent-2': {
2388
+ id: 'agent-2',
2389
+ pType: 'Agent',
2390
+ hasLeft: false,
2391
+ isConsulted: true,
2392
+ consultState: 'conferencing',
2393
+ },
2394
+ 'agent-3': {
2395
+ id: 'agent-3',
2396
+ pType: 'Agent',
2397
+ hasLeft: false,
2398
+ isConsulted: true,
2399
+ consultState: 'conferencing',
2400
+ },
2401
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2402
+ },
2403
+ media: {
2404
+ 'interaction-1': {
2405
+ mediaResourceId: 'interaction-1',
2406
+ mType: 'mainCall',
2407
+ participants: ['agent-1', 'agent-2', 'agent-3', 'customer-1'],
2408
+ isHold: false,
2409
+ },
2410
+ },
2411
+ } as any,
2412
+ });
2413
+
2414
+ const consultingTaskData = createTaskData({
2415
+ agentId: 'agent-3',
2416
+ isConsulted: true,
2417
+ consultingAgentId: 'agent-1',
2418
+ interactionId: 'interaction-1',
2419
+ mediaResourceId: 'interaction-1',
2420
+ consultMediaResourceId: 'consult-media-1',
2421
+ interaction: {
2422
+ state: 'conference',
2423
+ owner: 'agent-1',
2424
+ mainInteractionId: 'interaction-1',
2425
+ interactionId: 'interaction-1',
2426
+ participants: {
2427
+ 'agent-1': {
2428
+ id: 'agent-1',
2429
+ pType: 'Agent',
2430
+ hasLeft: false,
2431
+ consultState: 'consulting',
2432
+ },
2433
+ 'agent-2': {
2434
+ id: 'agent-2',
2435
+ pType: 'Agent',
2436
+ hasLeft: false,
2437
+ isConsulted: true,
2438
+ consultState: 'conferencing',
2439
+ },
2440
+ 'agent-3': {
2441
+ id: 'agent-3',
2442
+ pType: 'Agent',
2443
+ hasLeft: false,
2444
+ isConsulted: true,
2445
+ consultState: 'consulting',
2446
+ },
2447
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2448
+ },
2449
+ media: {
2450
+ 'interaction-1': {
2451
+ mediaResourceId: 'interaction-1',
2452
+ mType: 'mainCall',
2453
+ participants: ['agent-1', 'agent-2', 'customer-1'],
2454
+ isHold: false,
2455
+ },
2456
+ 'consult-media-1': {
2457
+ mediaResourceId: 'consult-media-1',
2458
+ mType: 'consult',
2459
+ participants: ['agent-1', 'agent-3'],
2460
+ isHold: false,
2461
+ },
2462
+ },
2463
+ } as any,
2464
+ });
2465
+
2466
+ const transferSuccessTaskData = createTaskData({
2467
+ agentId: 'agent-3',
2468
+ isConsulted: true,
2469
+ consultingAgentId: 'agent-1',
2470
+ wrapUpRequired: false,
2471
+ type: 'AgentConferenceTransferred' as any,
2472
+ interactionId: 'interaction-1',
2473
+ mediaResourceId: 'interaction-1',
2474
+ interaction: {
2475
+ state: 'conference',
2476
+ owner: 'agent-1',
2477
+ mainInteractionId: 'interaction-1',
2478
+ interactionId: 'interaction-1',
2479
+ participants: {
2480
+ 'agent-1': {
2481
+ id: 'agent-1',
2482
+ pType: 'Agent',
2483
+ hasLeft: true,
2484
+ consultState: 'conferencing',
2485
+ },
2486
+ 'agent-2': {
2487
+ id: 'agent-2',
2488
+ pType: 'Agent',
2489
+ hasLeft: false,
2490
+ isConsulted: true,
2491
+ consultState: 'conferencing',
2492
+ },
2493
+ 'agent-3': {
2494
+ id: 'agent-3',
2495
+ pType: 'Agent',
2496
+ hasLeft: false,
2497
+ isConsulted: true,
2498
+ consultState: 'consulting',
2499
+ },
2500
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2501
+ },
2502
+ media: {
2503
+ 'interaction-1': {
2504
+ mediaResourceId: 'interaction-1',
2505
+ mType: 'mainCall',
2506
+ participants: ['agent-2', 'agent-3', 'customer-1'],
2507
+ isHold: false,
2508
+ },
2509
+ },
2510
+ } as any,
2511
+ });
2512
+
2513
+ const service = startAgent3Machine();
2514
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2515
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2516
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2517
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2518
+
2519
+ service.send({
2520
+ type: TaskEvent.CONSULTING_ACTIVE,
2521
+ consultDestinationAgentJoined: true,
2522
+ taskData: consultingTaskData,
2523
+ });
2524
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
2525
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
2526
+
2527
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
2528
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2529
+ });
2530
+
2531
+ it('passive observer stays in CONFERENCING when another agent conference-transfers', () => {
2532
+ const service = startAgent2Machine();
2533
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
2534
+ const transferSuccessTaskData = createTaskData({
2535
+ agentId: 'agent-2',
2536
+ isConsulted: true,
2537
+ type: 'AgentConferenceTransferred' as any,
2538
+ interactionId: 'interaction-1',
2539
+ mediaResourceId: 'interaction-1',
2540
+ interaction: {
2541
+ state: 'conference',
2542
+ owner: 'agent-1',
2543
+ mainInteractionId: 'interaction-1',
2544
+ interactionId: 'interaction-1',
2545
+ participants: {
2546
+ 'agent-1': {
2547
+ id: 'agent-1',
2548
+ pType: 'Agent',
2549
+ hasLeft: false,
2550
+ consultState: 'conferencing',
2551
+ },
2552
+ 'agent-2': {
2553
+ id: 'agent-2',
2554
+ pType: 'Agent',
2555
+ hasLeft: false,
2556
+ isConsulted: true,
2557
+ consultState: 'conferencing',
2558
+ },
2559
+ 'agent-3': {
2560
+ id: 'agent-3',
2561
+ pType: 'Agent',
2562
+ hasLeft: false,
2563
+ isConsulted: true,
2564
+ consultState: 'conferencing',
2565
+ },
2566
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2567
+ },
2568
+ media: {
2569
+ 'interaction-1': {
2570
+ mediaResourceId: 'interaction-1',
2571
+ mType: 'mainCall',
2572
+ participants: ['agent-1', 'agent-2', 'agent-3', 'customer-1'],
2573
+ isHold: false,
2574
+ },
2575
+ },
2576
+ } as any,
2577
+ });
2578
+
2579
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2580
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2581
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2582
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2583
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
2584
+
2585
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
2586
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2587
+ });
2588
+
2589
+ it('terminates secondary agent on widgets-initiated conference transfer regression', () => {
2590
+ const service = startAgent2Machine();
2591
+ setupSecondaryAgentConsultingFromConference(service);
2592
+
2593
+ const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
2594
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE});
2595
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
2596
+
2597
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2598
+ });
2599
+
2600
+ it('terminates secondary agent on PARTICIPANT_LEAVE while in CONSULTING during conference', () => {
2601
+ const service = startAgent2Machine();
2602
+ setupSecondaryAgentDesktopConsultingFromConference(service);
2603
+
2604
+ const selfLeftTaskData = createTaskData({
2605
+ agentId: 'agent-2',
2606
+ isConsulted: true,
2607
+ participantId: 'agent-2',
2608
+ interactionId: 'interaction-1',
2609
+ mediaResourceId: 'interaction-1',
2610
+ interaction: {
2611
+ state: 'conference',
2612
+ owner: 'agent-1',
2613
+ mainInteractionId: 'interaction-1',
2614
+ interactionId: 'interaction-1',
2615
+ participants: {
2616
+ 'agent-1': {
2617
+ id: 'agent-1',
2618
+ pType: 'Agent',
2619
+ hasLeft: false,
2620
+ consultState: 'conferencing',
2621
+ },
2622
+ 'agent-3': {
2623
+ id: 'agent-3',
2624
+ pType: 'Agent',
2625
+ hasLeft: false,
2626
+ isConsulted: true,
2627
+ consultState: 'consulting',
2628
+ },
2629
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2630
+ },
2631
+ media: {
2632
+ 'interaction-1': {
2633
+ mediaResourceId: 'interaction-1',
2634
+ mType: 'mainCall',
2635
+ participants: ['agent-1', 'agent-3', 'customer-1'],
2636
+ isHold: false,
2637
+ },
2638
+ },
2639
+ } as any,
2640
+ });
2641
+
2642
+ service.send({
2643
+ type: TaskEvent.PARTICIPANT_LEAVE,
2644
+ taskData: selfLeftTaskData,
2645
+ participantId: 'agent-2',
2646
+ });
2647
+
2648
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2649
+ });
2650
+ });
2651
+ });