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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/.sdd/manifest.json +5 -4
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +5 -1
  4. package/ai-docs/SECURITY.md +1 -1
  5. package/ai-docs/contact-center-spec.md +22 -4
  6. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  7. package/dist/cc.js +440 -10
  8. package/dist/cc.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +10 -2
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics/behavioral-events.js +13 -0
  15. package/dist/metrics/behavioral-events.js.map +1 -1
  16. package/dist/metrics/constants.js +2 -0
  17. package/dist/metrics/constants.js.map +1 -1
  18. package/dist/services/AddressBook.js +18 -15
  19. package/dist/services/AddressBook.js.map +1 -1
  20. package/dist/services/AnswerCallOnWebexService.js +174 -0
  21. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  22. package/dist/services/EntryPoint.js +46 -68
  23. package/dist/services/EntryPoint.js.map +1 -1
  24. package/dist/services/Queue.js +27 -22
  25. package/dist/services/Queue.js.map +1 -1
  26. package/dist/services/WebexCrossClientService.js +171 -0
  27. package/dist/services/WebexCrossClientService.js.map +1 -0
  28. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  29. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  30. package/dist/services/config/Util.js +3 -0
  31. package/dist/services/config/Util.js.map +1 -1
  32. package/dist/services/config/constants.js +10 -6
  33. package/dist/services/config/constants.js.map +1 -1
  34. package/dist/services/config/types.js +4 -0
  35. package/dist/services/config/types.js.map +1 -1
  36. package/dist/services/core/Err.js.map +1 -1
  37. package/dist/services/core/WebexRequest.js +6 -2
  38. package/dist/services/core/WebexRequest.js.map +1 -1
  39. package/dist/services/core/aqm-reqs.js +28 -14
  40. package/dist/services/core/aqm-reqs.js.map +1 -1
  41. package/dist/services/core/types.js.map +1 -1
  42. package/dist/services/task/Task.js +73 -7
  43. package/dist/services/task/Task.js.map +1 -1
  44. package/dist/services/task/TaskFactory.js +8 -4
  45. package/dist/services/task/TaskFactory.js.map +1 -1
  46. package/dist/services/task/TaskManager.js +176 -20
  47. package/dist/services/task/TaskManager.js.map +1 -1
  48. package/dist/services/task/WebexCallingUtils.js +70 -0
  49. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  50. package/dist/services/task/constants.js +4 -1
  51. package/dist/services/task/constants.js.map +1 -1
  52. package/dist/services/task/contact.js +29 -0
  53. package/dist/services/task/contact.js.map +1 -1
  54. package/dist/services/task/digital/Digital.js +3 -2
  55. package/dist/services/task/digital/Digital.js.map +1 -1
  56. package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
  57. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  58. package/dist/services/task/state-machine/actions.js +6 -1
  59. package/dist/services/task/state-machine/actions.js.map +1 -1
  60. package/dist/services/task/state-machine/guards.js +38 -11
  61. package/dist/services/task/state-machine/guards.js.map +1 -1
  62. package/dist/services/task/state-machine/types.js.map +1 -1
  63. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  64. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  65. package/dist/services/task/types.js +16 -1
  66. package/dist/services/task/types.js.map +1 -1
  67. package/dist/services/task/voice/Voice.js +350 -14
  68. package/dist/services/task/voice/Voice.js.map +1 -1
  69. package/dist/services/task/voice/WebRTC.js +4 -1
  70. package/dist/services/task/voice/WebRTC.js.map +1 -1
  71. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  72. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  73. package/dist/services/wxAppTelephonyUtils.js +19 -0
  74. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  75. package/dist/types/cc.d.ts +77 -4
  76. package/dist/types/config.d.ts +7 -0
  77. package/dist/types/constants.d.ts +8 -0
  78. package/dist/types/index.d.ts +3 -1
  79. package/dist/types/metrics/constants.d.ts +2 -0
  80. package/dist/types/services/AddressBook.d.ts +2 -1
  81. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  82. package/dist/types/services/EntryPoint.d.ts +7 -5
  83. package/dist/types/services/Queue.d.ts +5 -3
  84. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  85. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  86. package/dist/types/services/config/constants.d.ts +8 -5
  87. package/dist/types/services/config/types.d.ts +17 -4
  88. package/dist/types/services/core/Err.d.ts +2 -0
  89. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  90. package/dist/types/services/core/types.d.ts +2 -0
  91. package/dist/types/services/task/Task.d.ts +20 -2
  92. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  93. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  94. package/dist/types/services/task/constants.d.ts +3 -0
  95. package/dist/types/services/task/contact.d.ts +4 -0
  96. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  97. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
  98. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  99. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  100. package/dist/types/services/task/types.d.ts +81 -4
  101. package/dist/types/services/task/voice/Voice.d.ts +48 -9
  102. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  103. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  104. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  105. package/dist/types/types.d.ts +14 -8
  106. package/dist/types/utils/PageCache.d.ts +20 -3
  107. package/dist/types.js +7 -2
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/PageCache.js +19 -5
  110. package/dist/utils/PageCache.js.map +1 -1
  111. package/dist/webex.js +1 -1
  112. package/package.json +1 -1
  113. package/src/cc.ts +506 -9
  114. package/src/config.ts +7 -0
  115. package/src/constants.ts +8 -0
  116. package/src/index.ts +2 -0
  117. package/src/metrics/ai-docs/metrics-spec.md +9 -3
  118. package/src/metrics/behavioral-events.ts +14 -0
  119. package/src/metrics/constants.ts +2 -0
  120. package/src/services/AddressBook.ts +17 -6
  121. package/src/services/AnswerCallOnWebexService.ts +206 -0
  122. package/src/services/EntryPoint.ts +59 -60
  123. package/src/services/Queue.ts +29 -12
  124. package/src/services/WebexCrossClientService.ts +212 -0
  125. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  126. package/src/services/ai-docs/AGENTS.md +10 -10
  127. package/src/services/ai-docs/services-spec.md +6 -1
  128. package/src/services/config/Util.ts +3 -0
  129. package/src/services/config/ai-docs/AGENTS.md +1 -1
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  131. package/src/services/config/ai-docs/config-spec.md +6 -0
  132. package/src/services/config/constants.ts +10 -6
  133. package/src/services/config/types.ts +16 -4
  134. package/src/services/core/Err.ts +1 -0
  135. package/src/services/core/WebexRequest.ts +3 -1
  136. package/src/services/core/ai-docs/core-spec.md +5 -1
  137. package/src/services/core/aqm-reqs.ts +30 -15
  138. package/src/services/core/types.ts +2 -0
  139. package/src/services/task/Task.ts +79 -8
  140. package/src/services/task/TaskFactory.ts +8 -3
  141. package/src/services/task/TaskManager.ts +254 -15
  142. package/src/services/task/WebexCallingUtils.ts +136 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  144. package/src/services/task/ai-docs/task-spec.md +117 -2
  145. package/src/services/task/constants.ts +3 -0
  146. package/src/services/task/contact.ts +30 -0
  147. package/src/services/task/digital/Digital.ts +4 -2
  148. package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
  149. package/src/services/task/state-machine/actions.ts +5 -2
  150. package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
  151. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
  152. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +46 -22
  153. package/src/services/task/state-machine/guards.ts +64 -11
  154. package/src/services/task/state-machine/types.ts +16 -1
  155. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  156. package/src/services/task/types.ts +114 -4
  157. package/src/services/task/voice/Voice.ts +462 -12
  158. package/src/services/task/voice/WebRTC.ts +5 -1
  159. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  160. package/src/services/wxAppTelephonyUtils.ts +14 -0
  161. package/src/types.ts +33 -9
  162. package/src/utils/AGENTS.md +21 -10
  163. package/src/utils/PageCache.ts +38 -5
  164. package/src/utils/ai-docs/utils-spec.md +21 -11
  165. package/test/unit/spec/cc.ts +1274 -0
  166. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  167. package/test/unit/spec/services/AddressBook.ts +37 -6
  168. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  169. package/test/unit/spec/services/EntryPoint.ts +87 -40
  170. package/test/unit/spec/services/Queue.ts +123 -12
  171. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  172. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  173. package/test/unit/spec/services/config/Util.ts +85 -0
  174. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  175. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  176. package/test/unit/spec/services/task/Task.ts +200 -0
  177. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  178. package/test/unit/spec/services/task/TaskManager.ts +653 -1
  179. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  180. package/test/unit/spec/services/task/contact.ts +33 -0
  181. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +854 -72
  182. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  183. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  184. package/test/unit/spec/services/task/voice/Voice.ts +874 -0
  185. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  186. package/umd/contact-center.min.js +2 -2
  187. package/umd/contact-center.min.js.map +1 -1
@@ -0,0 +1,459 @@
1
+ import {expect} from '@jest/globals';
2
+ import {
3
+ isWebexAppCallingOffer,
4
+ getCallingDeviceDetails,
5
+ getWebexCallingCallId,
6
+ acceptOnWebex,
7
+ rejectOnWebex,
8
+ toggleMuteOnWebex,
9
+ transmitDtmfOnWebex,
10
+ runWxAppAccept,
11
+ runWxAppReject,
12
+ mapWxAppVoiceError,
13
+ WxAppVoiceDeps,
14
+ WxAppVoiceLifecycle,
15
+ } from '../../../../../../src/services/task/voice/wxAppVoiceMethods';
16
+ import {METHODS} from '../../../../../../src/constants';
17
+ import {TaskState} from '../../../../../../src/services/task/state-machine';
18
+ import {TaskData} from '../../../../../../src/services/task/types';
19
+ import AnswerCallOnWebexService from '../../../../../../src/services/AnswerCallOnWebexService';
20
+
21
+ const WX_APP_DEVICE = {
22
+ deviceType: 'wxApp',
23
+ deviceId: 'device-id-1',
24
+ deviceCallId: 'call-id-1',
25
+ };
26
+
27
+ function makeParticipants() {
28
+ return {
29
+ 'agent-1': {id: 'agent-1', ...WX_APP_DEVICE},
30
+ 'customer-1': {id: 'customer-1', pType: 'Customer'},
31
+ };
32
+ }
33
+
34
+ function makeTaskData(overrides: Partial<TaskData> = {}): TaskData {
35
+ return {
36
+ interactionId: 'interaction-1',
37
+ agentId: 'agent-1',
38
+ mediaResourceId: 'media-1',
39
+ interaction: {
40
+ participants: makeParticipants(),
41
+ },
42
+ ...overrides,
43
+ } as unknown as TaskData;
44
+ }
45
+
46
+ function makeMockService(): jest.Mocked<AnswerCallOnWebexService> {
47
+ return {
48
+ answerCall: jest.fn().mockResolvedValue({}),
49
+ rejectCall: jest.fn().mockResolvedValue({}),
50
+ muteCall: jest.fn().mockResolvedValue({}),
51
+ unmuteCall: jest.fn().mockResolvedValue({}),
52
+ transmitDtmf: jest.fn().mockResolvedValue({}),
53
+ } as unknown as jest.Mocked<AnswerCallOnWebexService>;
54
+ }
55
+
56
+ function makeDeps(overrides: Partial<WxAppVoiceDeps> = {}): WxAppVoiceDeps {
57
+ let muted = false;
58
+
59
+ return {
60
+ enableWxBetterTogether: true,
61
+ answerCallOnWebexService: makeMockService(),
62
+ agentId: 'agent-1',
63
+ getTaskData: () => makeTaskData(),
64
+ getTaskState: () => TaskState.OFFERED,
65
+ getWxAppMuted: () => muted,
66
+ setWxAppMuted: (val) => {
67
+ muted = val;
68
+ },
69
+ ...overrides,
70
+ };
71
+ }
72
+
73
+ function makeLifecycle(overrides: Partial<WxAppVoiceLifecycle> = {}): WxAppVoiceLifecycle {
74
+ return {
75
+ setWxAppAcceptInFlight: jest.fn(),
76
+ setWxAppAnswerPending: jest.fn(),
77
+ resetWxAppMuted: jest.fn(),
78
+ syncWxAppMuteFromCallDetails: jest.fn().mockResolvedValue(undefined),
79
+ mapWxAppVoiceError: jest.fn((error: unknown) => {
80
+ throw error;
81
+ }),
82
+ ...overrides,
83
+ };
84
+ }
85
+
86
+ describe('isWebexAppCallingOffer', () => {
87
+ it('returns true when enableWxBetterTogether is true, state is OFFERED, and wxApp device details exist', () => {
88
+ expect(isWebexAppCallingOffer(makeDeps())).toBe(true);
89
+ });
90
+
91
+ it('returns false when enableWxBetterTogether is false', () => {
92
+ expect(isWebexAppCallingOffer(makeDeps({enableWxBetterTogether: false}))).toBe(false);
93
+ });
94
+
95
+ it('returns false when task state is not OFFERED', () => {
96
+ expect(isWebexAppCallingOffer(makeDeps({getTaskState: () => TaskState.CONNECTED}))).toBe(false);
97
+ });
98
+
99
+ it('returns false when no wxApp participant is found', () => {
100
+ const deps = makeDeps({
101
+ getTaskData: () =>
102
+ makeTaskData({
103
+ interaction: {
104
+ participants: {'customer-1': {id: 'customer-1'}},
105
+ } as any,
106
+ }),
107
+ });
108
+
109
+ expect(isWebexAppCallingOffer(deps)).toBe(false);
110
+ });
111
+ });
112
+
113
+ describe('getCallingDeviceDetails', () => {
114
+ it('returns device details for wxApp participant', () => {
115
+ const result = getCallingDeviceDetails(makeDeps());
116
+
117
+ expect(result).toEqual({
118
+ deviceType: 'wxApp',
119
+ deviceId: 'device-id-1',
120
+ deviceCallId: 'call-id-1',
121
+ });
122
+ });
123
+
124
+ it('returns undefined when no wxApp participant is present', () => {
125
+ const deps = makeDeps({
126
+ getTaskData: () =>
127
+ makeTaskData({
128
+ interaction: {
129
+ participants: {'customer-1': {id: 'customer-1'}},
130
+ } as any,
131
+ }),
132
+ });
133
+
134
+ expect(getCallingDeviceDetails(deps)).toBeUndefined();
135
+ });
136
+ });
137
+
138
+ describe('getWebexCallingCallId', () => {
139
+ it('returns null when state is OFFERED', () => {
140
+ expect(getWebexCallingCallId(makeDeps({getTaskState: () => TaskState.OFFERED}))).toBeNull();
141
+ });
142
+
143
+ it('returns null when state is IDLE', () => {
144
+ expect(getWebexCallingCallId(makeDeps({getTaskState: () => TaskState.IDLE}))).toBeNull();
145
+ });
146
+
147
+ it('returns null when state is TERMINATED', () => {
148
+ expect(getWebexCallingCallId(makeDeps({getTaskState: () => TaskState.TERMINATED}))).toBeNull();
149
+ });
150
+
151
+ it('returns null when state is COMPLETED', () => {
152
+ expect(getWebexCallingCallId(makeDeps({getTaskState: () => TaskState.COMPLETED}))).toBeNull();
153
+ });
154
+
155
+ it('returns null when state is WRAPPING_UP', () => {
156
+ expect(getWebexCallingCallId(makeDeps({getTaskState: () => TaskState.WRAPPING_UP}))).toBeNull();
157
+ });
158
+
159
+ it('returns the deviceCallId when state is CONNECTED', () => {
160
+ expect(getWebexCallingCallId(makeDeps({getTaskState: () => TaskState.CONNECTED}))).toBe(
161
+ 'call-id-1'
162
+ );
163
+ });
164
+
165
+ it('returns null when state is CONNECTED but no wxApp participant', () => {
166
+ const deps = makeDeps({
167
+ getTaskState: () => TaskState.CONNECTED,
168
+ getTaskData: () =>
169
+ makeTaskData({
170
+ interaction: {
171
+ participants: {'customer-1': {id: 'customer-1'}},
172
+ } as any,
173
+ }),
174
+ });
175
+
176
+ expect(getWebexCallingCallId(deps)).toBeNull();
177
+ });
178
+ });
179
+
180
+ describe('acceptOnWebex', () => {
181
+ it('calls answerCall with deviceCallId and deviceId', async () => {
182
+ const mockSvc = makeMockService();
183
+ const deps = makeDeps({answerCallOnWebexService: mockSvc});
184
+
185
+ await acceptOnWebex(deps);
186
+
187
+ expect(mockSvc.answerCall).toHaveBeenCalledWith({
188
+ callId: 'call-id-1',
189
+ endpointId: 'device-id-1',
190
+ lineOwnerId: undefined,
191
+ });
192
+ });
193
+
194
+ it('passes lineOwnerId when provided', async () => {
195
+ const mockSvc = makeMockService();
196
+ const deps = makeDeps({answerCallOnWebexService: mockSvc});
197
+
198
+ await acceptOnWebex(deps, {lineOwnerId: 'lo-1'});
199
+
200
+ expect(mockSvc.answerCall).toHaveBeenCalledWith(
201
+ expect.objectContaining({lineOwnerId: 'lo-1'})
202
+ );
203
+ });
204
+
205
+ it('defaults lineOwnerId from encoded participant owner when not provided', async () => {
206
+ const encodedOwner = Buffer.from('people/lo-decoded').toString('base64');
207
+ const mockSvc = makeMockService();
208
+ const deps = makeDeps({
209
+ answerCallOnWebexService: mockSvc,
210
+ getTaskData: () =>
211
+ makeTaskData({
212
+ interaction: {
213
+ participants: {
214
+ 'agent-1': {id: 'agent-1', ...WX_APP_DEVICE, lineOwnerId: encodedOwner},
215
+ 'customer-1': {id: 'customer-1', pType: 'Customer'},
216
+ },
217
+ } as any,
218
+ }),
219
+ });
220
+
221
+ await acceptOnWebex(deps);
222
+
223
+ expect(mockSvc.answerCall).toHaveBeenCalledWith(
224
+ expect.objectContaining({lineOwnerId: 'lo-decoded'})
225
+ );
226
+ });
227
+
228
+ it('throws when enableWxBetterTogether is false', async () => {
229
+ const deps = makeDeps({enableWxBetterTogether: false});
230
+
231
+ await expect(acceptOnWebex(deps)).rejects.toThrow();
232
+ });
233
+
234
+ it('throws when task is not in OFFERED state', async () => {
235
+ const deps = makeDeps({getTaskState: () => TaskState.CONNECTED});
236
+
237
+ await expect(acceptOnWebex(deps)).rejects.toThrow();
238
+ });
239
+
240
+ it('throws when device details are unavailable', async () => {
241
+ const deps = makeDeps({
242
+ getTaskData: () =>
243
+ makeTaskData({
244
+ interaction: {
245
+ participants: {'customer-1': {id: 'customer-1'}},
246
+ } as any,
247
+ }),
248
+ });
249
+
250
+ await expect(acceptOnWebex(deps)).rejects.toThrow();
251
+ });
252
+ });
253
+
254
+ describe('rejectOnWebex', () => {
255
+ it('calls rejectCall with deviceCallId', async () => {
256
+ const mockSvc = makeMockService();
257
+ const deps = makeDeps({answerCallOnWebexService: mockSvc});
258
+
259
+ await rejectOnWebex(deps);
260
+
261
+ expect(mockSvc.rejectCall).toHaveBeenCalledWith({
262
+ callId: 'call-id-1',
263
+ lineOwnerId: undefined,
264
+ });
265
+ });
266
+
267
+ it('throws when task is not in OFFERED state', async () => {
268
+ const deps = makeDeps({getTaskState: () => TaskState.CONNECTED});
269
+
270
+ await expect(rejectOnWebex(deps)).rejects.toThrow();
271
+ });
272
+
273
+ it('throws when enableWxBetterTogether is false', async () => {
274
+ const deps = makeDeps({enableWxBetterTogether: false});
275
+
276
+ await expect(rejectOnWebex(deps)).rejects.toThrow();
277
+ });
278
+ });
279
+
280
+ describe('toggleMuteOnWebex', () => {
281
+ it('calls muteCall when not currently muted', async () => {
282
+ const mockSvc = makeMockService();
283
+ let muted = false;
284
+ const deps = makeDeps({
285
+ answerCallOnWebexService: mockSvc,
286
+ getTaskState: () => TaskState.CONNECTED,
287
+ getWxAppMuted: () => muted,
288
+ setWxAppMuted: (val) => {
289
+ muted = val;
290
+ },
291
+ });
292
+
293
+ await toggleMuteOnWebex(deps);
294
+
295
+ expect(mockSvc.muteCall).toHaveBeenCalledWith({callId: 'call-id-1', lineOwnerId: undefined});
296
+ expect(muted).toBe(true);
297
+ });
298
+
299
+ it('calls unmuteCall when currently muted', async () => {
300
+ const mockSvc = makeMockService();
301
+ let muted = true;
302
+ const deps = makeDeps({
303
+ answerCallOnWebexService: mockSvc,
304
+ getTaskState: () => TaskState.CONNECTED,
305
+ getWxAppMuted: () => muted,
306
+ setWxAppMuted: (val) => {
307
+ muted = val;
308
+ },
309
+ });
310
+
311
+ await toggleMuteOnWebex(deps);
312
+
313
+ expect(mockSvc.unmuteCall).toHaveBeenCalled();
314
+ expect(muted).toBe(false);
315
+ });
316
+
317
+ it('throws when call ID is unavailable (state is OFFERED)', async () => {
318
+ const deps = makeDeps({getTaskState: () => TaskState.OFFERED});
319
+
320
+ await expect(toggleMuteOnWebex(deps)).rejects.toThrow('WxApp call ID is unavailable');
321
+ });
322
+
323
+ it('throws when enableWxBetterTogether is false', async () => {
324
+ const deps = makeDeps({enableWxBetterTogether: false, getTaskState: () => TaskState.CONNECTED});
325
+
326
+ await expect(toggleMuteOnWebex(deps)).rejects.toThrow();
327
+ });
328
+
329
+ it('uses options.muted=true even when internal wxAppMuted is true (desync regression)', async () => {
330
+ const mockSvc = makeMockService();
331
+ let muted = true;
332
+ const deps = makeDeps({
333
+ answerCallOnWebexService: mockSvc,
334
+ getTaskState: () => TaskState.CONNECTED,
335
+ getWxAppMuted: () => muted,
336
+ setWxAppMuted: (val) => {
337
+ muted = val;
338
+ },
339
+ });
340
+
341
+ await toggleMuteOnWebex(deps, {muted: true});
342
+
343
+ expect(mockSvc.muteCall).toHaveBeenCalledWith({callId: 'call-id-1', lineOwnerId: undefined});
344
+ expect(mockSvc.unmuteCall).not.toHaveBeenCalled();
345
+ expect(muted).toBe(true);
346
+ });
347
+
348
+ it('uses options.muted=false even when internal wxAppMuted is false (desync regression)', async () => {
349
+ const mockSvc = makeMockService();
350
+ let muted = false;
351
+ const deps = makeDeps({
352
+ answerCallOnWebexService: mockSvc,
353
+ getTaskState: () => TaskState.CONNECTED,
354
+ getWxAppMuted: () => muted,
355
+ setWxAppMuted: (val) => {
356
+ muted = val;
357
+ },
358
+ });
359
+
360
+ await toggleMuteOnWebex(deps, {muted: false});
361
+
362
+ expect(mockSvc.unmuteCall).toHaveBeenCalledWith({callId: 'call-id-1', lineOwnerId: undefined});
363
+ expect(mockSvc.muteCall).not.toHaveBeenCalled();
364
+ expect(muted).toBe(false);
365
+ });
366
+ });
367
+
368
+ describe('transmitDtmfOnWebex', () => {
369
+ it('calls transmitDtmf with callId and dtmf', async () => {
370
+ const mockSvc = makeMockService();
371
+ const deps = makeDeps({
372
+ answerCallOnWebexService: mockSvc,
373
+ getTaskState: () => TaskState.CONNECTED,
374
+ });
375
+
376
+ await transmitDtmfOnWebex(deps, {dtmf: '5'});
377
+
378
+ expect(mockSvc.transmitDtmf).toHaveBeenCalledWith({
379
+ callId: 'call-id-1',
380
+ dtmf: '5',
381
+ lineOwnerId: undefined,
382
+ });
383
+ });
384
+
385
+ it('passes lineOwnerId when provided', async () => {
386
+ const mockSvc = makeMockService();
387
+ const deps = makeDeps({
388
+ answerCallOnWebexService: mockSvc,
389
+ getTaskState: () => TaskState.CONNECTED,
390
+ });
391
+
392
+ await transmitDtmfOnWebex(deps, {dtmf: '9', lineOwnerId: 'lo-1'});
393
+
394
+ expect(mockSvc.transmitDtmf).toHaveBeenCalledWith(
395
+ expect.objectContaining({lineOwnerId: 'lo-1'})
396
+ );
397
+ });
398
+
399
+ it('throws when call ID is unavailable', async () => {
400
+ const deps = makeDeps({getTaskState: () => TaskState.OFFERED});
401
+
402
+ await expect(transmitDtmfOnWebex(deps, {dtmf: '1'})).rejects.toThrow(
403
+ 'WxApp call ID is unavailable'
404
+ );
405
+ });
406
+ });
407
+
408
+ describe('runWxAppAccept', () => {
409
+ it('sets in-flight and pending flags, resets mute, and syncs after answer', async () => {
410
+ const deps = makeDeps();
411
+ const lifecycle = makeLifecycle();
412
+
413
+ await runWxAppAccept(deps, lifecycle);
414
+
415
+ expect(lifecycle.setWxAppAcceptInFlight).toHaveBeenNthCalledWith(1, true);
416
+ expect(lifecycle.setWxAppAnswerPending).toHaveBeenCalledWith(true);
417
+ expect(deps.answerCallOnWebexService!.answerCall).toHaveBeenCalled();
418
+ expect(lifecycle.resetWxAppMuted).toHaveBeenCalled();
419
+ expect(lifecycle.syncWxAppMuteFromCallDetails).toHaveBeenCalled();
420
+ expect(lifecycle.setWxAppAcceptInFlight).toHaveBeenLastCalledWith(false);
421
+ });
422
+
423
+ it('clears pending and maps error when answer fails', async () => {
424
+ const deps = makeDeps();
425
+ const error = new Error('answer failed');
426
+ deps.answerCallOnWebexService!.answerCall = jest.fn().mockRejectedValue(error);
427
+ const lifecycle = makeLifecycle();
428
+
429
+ await expect(runWxAppAccept(deps, lifecycle)).rejects.toThrow('answer failed');
430
+
431
+ expect(lifecycle.setWxAppAnswerPending).toHaveBeenCalledWith(false);
432
+ expect(lifecycle.mapWxAppVoiceError).toHaveBeenCalledWith(error, METHODS.ACCEPT);
433
+ expect(lifecycle.setWxAppAcceptInFlight).toHaveBeenLastCalledWith(false);
434
+ });
435
+ });
436
+
437
+ describe('runWxAppReject', () => {
438
+ it('maps error when reject fails', async () => {
439
+ const deps = makeDeps();
440
+ const error = new Error('reject failed');
441
+ deps.answerCallOnWebexService!.rejectCall = jest.fn().mockRejectedValue(error);
442
+ const lifecycle = makeLifecycle();
443
+
444
+ await expect(runWxAppReject(deps, lifecycle)).rejects.toThrow('reject failed');
445
+
446
+ expect(lifecycle.mapWxAppVoiceError).toHaveBeenCalledWith(error, METHODS.REJECT);
447
+ });
448
+ });
449
+
450
+ describe('mapWxAppVoiceError', () => {
451
+ it('rethrows normalized wxApp telephony errors without remapping', () => {
452
+ const normalized = Object.assign(new Error('TELEPHONY_ERROR'), {
453
+ isWxAppTelephonyError: true,
454
+ trackingId: 'track-wxapp-1',
455
+ });
456
+
457
+ expect(() => mapWxAppVoiceError(normalized, 'accept', 'cc')).toThrow(normalized);
458
+ });
459
+ });