@webex/contact-center 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.10

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 (198) hide show
  1. package/.sdd/manifest.json +7 -5
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +8 -3
  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 +544 -12
  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/MetricsManager.js +16 -0
  15. package/dist/metrics/MetricsManager.js.map +1 -1
  16. package/dist/metrics/behavioral-events.js +104 -0
  17. package/dist/metrics/behavioral-events.js.map +1 -1
  18. package/dist/metrics/constants.js +18 -0
  19. package/dist/metrics/constants.js.map +1 -1
  20. package/dist/services/AddressBook.js +18 -15
  21. package/dist/services/AddressBook.js.map +1 -1
  22. package/dist/services/AnswerCallOnWebexService.js +202 -0
  23. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  24. package/dist/services/EntryPoint.js +46 -68
  25. package/dist/services/EntryPoint.js.map +1 -1
  26. package/dist/services/Queue.js +27 -22
  27. package/dist/services/Queue.js.map +1 -1
  28. package/dist/services/WebexCrossClientService.js +222 -0
  29. package/dist/services/WebexCrossClientService.js.map +1 -0
  30. package/dist/services/WxAppTelephonyMercurySync.js +108 -0
  31. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  32. package/dist/services/config/Util.js +3 -0
  33. package/dist/services/config/Util.js.map +1 -1
  34. package/dist/services/config/constants.js +10 -6
  35. package/dist/services/config/constants.js.map +1 -1
  36. package/dist/services/config/types.js +4 -0
  37. package/dist/services/config/types.js.map +1 -1
  38. package/dist/services/core/Err.js.map +1 -1
  39. package/dist/services/core/WebexRequest.js +6 -2
  40. package/dist/services/core/WebexRequest.js.map +1 -1
  41. package/dist/services/core/aqm-reqs.js +28 -14
  42. package/dist/services/core/aqm-reqs.js.map +1 -1
  43. package/dist/services/core/types.js.map +1 -1
  44. package/dist/services/task/Task.js +73 -7
  45. package/dist/services/task/Task.js.map +1 -1
  46. package/dist/services/task/TaskFactory.js +8 -4
  47. package/dist/services/task/TaskFactory.js.map +1 -1
  48. package/dist/services/task/TaskManager.js +490 -21
  49. package/dist/services/task/TaskManager.js.map +1 -1
  50. package/dist/services/task/WebexCallingUtils.js +70 -0
  51. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  52. package/dist/services/task/constants.js +4 -1
  53. package/dist/services/task/constants.js.map +1 -1
  54. package/dist/services/task/contact.js +29 -0
  55. package/dist/services/task/contact.js.map +1 -1
  56. package/dist/services/task/digital/Digital.js +3 -2
  57. package/dist/services/task/digital/Digital.js.map +1 -1
  58. package/dist/services/task/state-machine/TaskStateMachine.js +162 -53
  59. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  60. package/dist/services/task/state-machine/actions.js +6 -1
  61. package/dist/services/task/state-machine/actions.js.map +1 -1
  62. package/dist/services/task/state-machine/guards.js +7 -10
  63. package/dist/services/task/state-machine/guards.js.map +1 -1
  64. package/dist/services/task/state-machine/types.js.map +1 -1
  65. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  66. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  67. package/dist/services/task/types.js +16 -1
  68. package/dist/services/task/types.js.map +1 -1
  69. package/dist/services/task/voice/Voice.js +456 -15
  70. package/dist/services/task/voice/Voice.js.map +1 -1
  71. package/dist/services/task/voice/WebRTC.js +4 -1
  72. package/dist/services/task/voice/WebRTC.js.map +1 -1
  73. package/dist/services/task/voice/wxAppVoiceMethods.js +354 -0
  74. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  75. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  76. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  77. package/dist/services/wxAppTelephonyUtils.js +19 -0
  78. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  79. package/dist/types/cc.d.ts +78 -4
  80. package/dist/types/config.d.ts +7 -0
  81. package/dist/types/constants.d.ts +8 -0
  82. package/dist/types/index.d.ts +3 -1
  83. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  84. package/dist/types/metrics/constants.d.ts +17 -0
  85. package/dist/types/services/AddressBook.d.ts +2 -1
  86. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  87. package/dist/types/services/EntryPoint.d.ts +7 -5
  88. package/dist/types/services/Queue.d.ts +5 -3
  89. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  90. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  91. package/dist/types/services/config/constants.d.ts +8 -5
  92. package/dist/types/services/config/types.d.ts +17 -4
  93. package/dist/types/services/core/Err.d.ts +2 -0
  94. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  95. package/dist/types/services/core/types.d.ts +2 -0
  96. package/dist/types/services/task/Task.d.ts +20 -2
  97. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  98. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  99. package/dist/types/services/task/constants.d.ts +3 -0
  100. package/dist/types/services/task/contact.d.ts +4 -0
  101. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  102. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +304 -56
  103. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  104. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  105. package/dist/types/services/task/types.d.ts +81 -4
  106. package/dist/types/services/task/voice/Voice.d.ts +52 -9
  107. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  108. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
  109. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  110. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  111. package/dist/types/types.d.ts +14 -8
  112. package/dist/types/utils/PageCache.d.ts +20 -3
  113. package/dist/types.js +7 -2
  114. package/dist/types.js.map +1 -1
  115. package/dist/utils/PageCache.js +19 -5
  116. package/dist/utils/PageCache.js.map +1 -1
  117. package/dist/webex.js +1 -1
  118. package/package.json +8 -8
  119. package/src/cc.ts +657 -12
  120. package/src/config.ts +7 -0
  121. package/src/constants.ts +8 -0
  122. package/src/index.ts +2 -0
  123. package/src/metrics/MetricsManager.ts +16 -0
  124. package/src/metrics/ai-docs/metrics-spec.md +78 -10
  125. package/src/metrics/behavioral-events.ts +106 -0
  126. package/src/metrics/constants.ts +20 -0
  127. package/src/services/AddressBook.ts +17 -6
  128. package/src/services/AnswerCallOnWebexService.ts +239 -0
  129. package/src/services/EntryPoint.ts +59 -60
  130. package/src/services/Queue.ts +29 -12
  131. package/src/services/WebexCrossClientService.ts +293 -0
  132. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  133. package/src/services/agent/ai-docs/agent-spec.md +1 -0
  134. package/src/services/ai-docs/AGENTS.md +10 -10
  135. package/src/services/ai-docs/services-spec.md +6 -1
  136. package/src/services/config/Util.ts +3 -0
  137. package/src/services/config/ai-docs/AGENTS.md +1 -1
  138. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  139. package/src/services/config/ai-docs/config-spec.md +6 -0
  140. package/src/services/config/constants.ts +10 -6
  141. package/src/services/config/types.ts +16 -4
  142. package/src/services/core/Err.ts +1 -0
  143. package/src/services/core/WebexRequest.ts +3 -1
  144. package/src/services/core/ai-docs/core-spec.md +5 -1
  145. package/src/services/core/aqm-reqs.ts +30 -15
  146. package/src/services/core/types.ts +2 -0
  147. package/src/services/task/Task.ts +79 -8
  148. package/src/services/task/TaskFactory.ts +8 -3
  149. package/src/services/task/TaskManager.ts +716 -17
  150. package/src/services/task/WebexCallingUtils.ts +136 -0
  151. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  152. package/src/services/task/ai-docs/task-spec.md +156 -3
  153. package/src/services/task/constants.ts +3 -0
  154. package/src/services/task/contact.ts +30 -0
  155. package/src/services/task/digital/Digital.ts +4 -2
  156. package/src/services/task/state-machine/TaskStateMachine.ts +235 -81
  157. package/src/services/task/state-machine/actions.ts +5 -2
  158. package/src/services/task/state-machine/ai-docs/AGENTS.md +6 -4
  159. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +35 -16
  160. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +103 -33
  161. package/src/services/task/state-machine/guards.ts +7 -10
  162. package/src/services/task/state-machine/types.ts +16 -1
  163. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  164. package/src/services/task/types.ts +114 -4
  165. package/src/services/task/voice/Voice.ts +595 -14
  166. package/src/services/task/voice/WebRTC.ts +5 -1
  167. package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
  168. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  169. package/src/services/wxAppTelephonyUtils.ts +14 -0
  170. package/src/types.ts +33 -9
  171. package/src/utils/AGENTS.md +21 -10
  172. package/src/utils/PageCache.ts +38 -5
  173. package/src/utils/ai-docs/utils-spec.md +21 -11
  174. package/test/unit/spec/cc.ts +1522 -19
  175. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  176. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  177. package/test/unit/spec/services/AddressBook.ts +37 -6
  178. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  179. package/test/unit/spec/services/EntryPoint.ts +87 -40
  180. package/test/unit/spec/services/Queue.ts +123 -12
  181. package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
  182. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  183. package/test/unit/spec/services/config/Util.ts +85 -0
  184. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  185. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  186. package/test/unit/spec/services/task/Task.ts +274 -6
  187. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  188. package/test/unit/spec/services/task/TaskManager.ts +1717 -9
  189. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  190. package/test/unit/spec/services/task/contact.ts +33 -0
  191. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1485 -109
  192. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  193. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  194. package/test/unit/spec/services/task/voice/Voice.ts +1412 -0
  195. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
  196. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  197. package/umd/contact-center.min.js +2 -2
  198. package/umd/contact-center.min.js.map +1 -1
@@ -252,6 +252,31 @@ describe('MetricsManagerImplementation', () => {
252
252
  });
253
253
  });
254
254
 
255
+ describe('cancelTimedEvent', () => {
256
+ it('should clear a running timer without attaching duration to a later unrelated event', () => {
257
+ jest.spyOn(MetricsManager.prototype as any, 'isMetricsDisabled').mockReturnValue(false);
258
+ metricsManager.timeEvent(['timedSuccess', 'timedFailed']);
259
+ metricsManager.cancelTimedEvent(['timedSuccess', 'timedFailed']);
260
+
261
+ const result = metricsManager['addDurationIfTimed']('timedFailed');
262
+
263
+ expect(result).toEqual({});
264
+ expect(metricsManager['runningEvents']['timedSuccess']).toBeUndefined();
265
+ });
266
+
267
+ it('should not clear timers when metrics are disabled', () => {
268
+ jest.spyOn(MetricsManager.prototype as any, 'isMetricsDisabled').mockReturnValue(true);
269
+ metricsManager['runningEvents']['event1'] = {
270
+ startTime: 100,
271
+ keys: new Set(['event1']),
272
+ };
273
+
274
+ metricsManager.cancelTimedEvent('event1');
275
+
276
+ expect(metricsManager['runningEvents']['event1']).toBeDefined();
277
+ });
278
+ });
279
+
255
280
  describe('addDurationIfTimed', () => {
256
281
  it('should return options when event name is not in runningEvents', () => {
257
282
  const options: EventPayload = {key: 'value'};
@@ -152,6 +152,24 @@ describe('metrics/behavioral-events', () => {
152
152
  verb: 'fail',
153
153
  });
154
154
 
155
+ expect(
156
+ getEventTaxonomy(METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS)
157
+ ).toEqual({
158
+ product,
159
+ agent: 'user',
160
+ target: 'task_conference_participant_drop',
161
+ verb: 'complete',
162
+ });
163
+
164
+ expect(
165
+ getEventTaxonomy(METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED)
166
+ ).toEqual({
167
+ product,
168
+ agent: 'user',
169
+ target: 'task_conference_participant_drop',
170
+ verb: 'fail',
171
+ });
172
+
155
173
  expect(getEventTaxonomy(METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS)).toEqual({
156
174
  product,
157
175
  agent: 'user',
@@ -166,6 +184,48 @@ describe('metrics/behavioral-events', () => {
166
184
  verb: 'fail',
167
185
  });
168
186
 
187
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS)).toEqual({
188
+ product,
189
+ agent: 'user',
190
+ target: 'task_accept_webex_together',
191
+ verb: 'complete',
192
+ });
193
+
194
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_SESSION_SKIPPED)).toEqual({
195
+ product,
196
+ agent: 'user',
197
+ target: 'webex_together_session_init',
198
+ verb: 'ignore',
199
+ });
200
+
201
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS)).toEqual({
202
+ product,
203
+ agent: 'user',
204
+ target: 'webex_together_usersub_publish',
205
+ verb: 'complete',
206
+ });
207
+
208
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED)).toEqual({
209
+ product,
210
+ agent: 'user',
211
+ target: 'webex_together_mercury_subscribe',
212
+ verb: 'fail',
213
+ });
214
+
215
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_SUCCESS)).toEqual({
216
+ product,
217
+ agent: 'user',
218
+ target: 'task_mute_webex_together',
219
+ verb: 'complete',
220
+ });
221
+
222
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_FAILED)).toEqual({
223
+ product,
224
+ agent: 'user',
225
+ target: 'task_dtmf_webex_together',
226
+ verb: 'fail',
227
+ });
228
+
169
229
  expect(getEventTaxonomy('' as METRIC_EVENT_NAMES)).toEqual(undefined);
170
230
  });
171
231
  });
@@ -99,7 +99,8 @@ describe('AddressBook', () => {
99
99
 
100
100
  expect(mockWebex.request).toHaveBeenCalledWith({
101
101
  service: 'wcc-api-gateway',
102
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100',
102
+ resource:
103
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100&sort=name%2CASC',
103
104
  method: HTTP_METHODS.GET,
104
105
  });
105
106
 
@@ -133,7 +134,8 @@ describe('AddressBook', () => {
133
134
  module: 'AddressBook',
134
135
  method: 'getEntries',
135
136
  data: {
136
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100',
137
+ resource:
138
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100&sort=name%2CASC',
137
139
  service: 'wcc-api-gateway',
138
140
  },
139
141
  });
@@ -155,7 +157,8 @@ describe('AddressBook', () => {
155
157
 
156
158
  expect(mockWebex.request).toHaveBeenCalledWith({
157
159
  service: 'wcc-api-gateway',
158
- resource: '/organization/test-org-id/v2/address-book/custom-book-id/entry?page=1&pageSize=25&filter=name%3D%3D%22John+Doe%22&attributes=id%2Cname%2Cnumber&search=john',
160
+ resource:
161
+ '/organization/test-org-id/v2/address-book/custom-book-id/entry?page=1&pageSize=25&filter=name%3D%3D%22John+Doe%22&attributes=id%2Cname%2Cnumber&search=john&sort=name%2CASC',
159
162
  method: HTTP_METHODS.GET,
160
163
  });
161
164
 
@@ -175,6 +178,31 @@ describe('AddressBook', () => {
175
178
  );
176
179
  });
177
180
 
181
+ it('should allow callers to override the default backend sorting and bypass the default page cache', async () => {
182
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
183
+
184
+ await addressBookAPI.getEntries({
185
+ page: 0,
186
+ pageSize: 25,
187
+ sortBy: 'name',
188
+ sortOrder: 'desc',
189
+ });
190
+ await addressBookAPI.getEntries({
191
+ page: 0,
192
+ pageSize: 25,
193
+ sortBy: 'name',
194
+ sortOrder: 'desc',
195
+ });
196
+
197
+ expect(mockWebex.request).toHaveBeenCalledTimes(2);
198
+ expect(mockWebex.request).toHaveBeenLastCalledWith({
199
+ service: 'wcc-api-gateway',
200
+ resource:
201
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=25&sort=name%2CDESC',
202
+ method: HTTP_METHODS.GET,
203
+ });
204
+ });
205
+
178
206
  it('should handle API errors and track metrics', async () => {
179
207
  (mockWebex.request as jest.Mock).mockRejectedValue(new Error('Internal Server Error'));
180
208
 
@@ -182,7 +210,8 @@ describe('AddressBook', () => {
182
210
 
183
211
  expect(mockWebex.request).toHaveBeenCalledWith({
184
212
  service: 'wcc-api-gateway',
185
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100',
213
+ resource:
214
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100&sort=name%2CASC',
186
215
  method: HTTP_METHODS.GET,
187
216
  });
188
217
 
@@ -316,14 +345,16 @@ describe('AddressBook', () => {
316
345
  expect(result.meta.page).toBe(1);
317
346
  expect(mockWebex.request).toHaveBeenCalledWith({
318
347
  service: 'wcc-api-gateway',
319
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100',
348
+ resource:
349
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100&sort=name%2CASC',
320
350
  method: HTTP_METHODS.GET,
321
351
  });
322
352
  expect(LoggerProxy.info).toHaveBeenCalledWith('Making API request to fetch address book entries', {
323
353
  module: 'AddressBook',
324
354
  method: 'getEntries',
325
355
  data: {
326
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100',
356
+ resource:
357
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100&sort=name%2CASC',
327
358
  service: 'wcc-api-gateway',
328
359
  },
329
360
  });
@@ -0,0 +1,264 @@
1
+ import {expect} from '@jest/globals';
2
+ import AnswerCallOnWebexService from '../../../../src/services/AnswerCallOnWebexService';
3
+ import {WebexSDK} from '../../../../src/types';
4
+ import LoggerProxy from '../../../../src/logger-proxy';
5
+
6
+ jest.mock('../../../../src/logger-proxy');
7
+ jest.mock('../../../../src/services/core/WebexRequest', () => ({
8
+ __esModule: true,
9
+ default: {
10
+ getInstance: () => ({uploadLogs: jest.fn()}),
11
+ },
12
+ }));
13
+
14
+ const TELEPHONY_BASE = 'https://api.ciscospark.com';
15
+ const CALLS_URL = `${TELEPHONY_BASE}/telephony/calls`;
16
+
17
+ function makeMockWebex(overrides: Partial<{_serviceUrls: Record<string, string>}> = {}): WebexSDK {
18
+ return {
19
+ request: jest.fn().mockResolvedValue({statusCode: 200, body: {}}),
20
+ internal: {
21
+ services: {
22
+ _serviceUrls: {hydra: TELEPHONY_BASE, ...overrides._serviceUrls},
23
+ get: jest.fn(),
24
+ },
25
+ },
26
+ } as unknown as WebexSDK;
27
+ }
28
+
29
+ describe('AnswerCallOnWebexService', () => {
30
+ let webex: WebexSDK;
31
+ let service: AnswerCallOnWebexService;
32
+
33
+ beforeEach(() => {
34
+ jest.clearAllMocks();
35
+ webex = makeMockWebex();
36
+ service = new AnswerCallOnWebexService(webex);
37
+ });
38
+
39
+ describe('getTelephonyBaseUrl resolution', () => {
40
+ it('uses hydra URL from _serviceUrls when available', async () => {
41
+ await service.answerCall({callId: 'c1', endpointId: 'e1'});
42
+
43
+ expect(webex.request).toHaveBeenCalledWith(
44
+ expect.objectContaining({uri: `${CALLS_URL}/answer`})
45
+ );
46
+ });
47
+
48
+ it('falls back to services.get("hydra") when _serviceUrls.hydra is missing', async () => {
49
+ const hydraBase = 'https://api.ciscospark.com/v1';
50
+ const get = jest.fn().mockReturnValue(hydraBase);
51
+ webex = {
52
+ request: jest.fn().mockResolvedValue({statusCode: 200, body: {}}),
53
+ internal: {
54
+ services: {
55
+ _serviceUrls: {},
56
+ get,
57
+ },
58
+ },
59
+ } as unknown as WebexSDK;
60
+ service = new AnswerCallOnWebexService(webex);
61
+
62
+ await service.answerCall({callId: 'c1', endpointId: 'e1'});
63
+
64
+ expect(get).toHaveBeenCalledWith('hydra');
65
+ expect(webex.request).toHaveBeenCalledWith(
66
+ expect.objectContaining({uri: `${hydraBase}/telephony/calls/answer`})
67
+ );
68
+ });
69
+
70
+ it('throws when hydra URL is unavailable', async () => {
71
+ webex = {
72
+ request: jest.fn(),
73
+ internal: {
74
+ services: {
75
+ _serviceUrls: {},
76
+ get: jest.fn().mockReturnValue(''),
77
+ },
78
+ },
79
+ } as unknown as WebexSDK;
80
+ service = new AnswerCallOnWebexService(webex);
81
+
82
+ await expect(service.answerCall({callId: 'c1', endpointId: 'e1'})).rejects.toBeDefined();
83
+ });
84
+ });
85
+
86
+ describe('answerCall', () => {
87
+ it('POST /answer with callId and endpointId', async () => {
88
+ await service.answerCall({callId: 'call-123', endpointId: 'ep-456'});
89
+
90
+ expect(webex.request).toHaveBeenCalledWith({
91
+ uri: `${CALLS_URL}/answer`,
92
+ method: 'POST',
93
+ body: {callId: 'call-123', endpointId: 'ep-456'},
94
+ addAuthHeader: true,
95
+ });
96
+ });
97
+
98
+ it('includes lineOwnerId when provided', async () => {
99
+ await service.answerCall({callId: 'c1', endpointId: 'e1', lineOwnerId: 'lo-1'});
100
+
101
+ expect(webex.request).toHaveBeenCalledWith(
102
+ expect.objectContaining({body: {callId: 'c1', endpointId: 'e1', lineOwnerId: 'lo-1'}})
103
+ );
104
+ });
105
+
106
+ it('omits lineOwnerId when not provided', async () => {
107
+ await service.answerCall({callId: 'c1', endpointId: 'e1'});
108
+
109
+ const call = (webex.request as jest.Mock).mock.calls[0][0];
110
+ expect(call.body).not.toHaveProperty('lineOwnerId');
111
+ });
112
+
113
+ it('rejects and logs on request failure', async () => {
114
+ const err = {
115
+ statusCode: 400,
116
+ details: {
117
+ trackingId: 'track-wxapp-1',
118
+ data: {reason: 'TELEPHONY_ERROR'},
119
+ },
120
+ };
121
+ (webex.request as jest.Mock).mockRejectedValue(err);
122
+
123
+ await expect(service.answerCall({callId: 'c1', endpointId: 'e1'})).rejects.toMatchObject({
124
+ isWxAppTelephonyError: true,
125
+ message: 'TELEPHONY_ERROR',
126
+ trackingId: 'track-wxapp-1',
127
+ status: 400,
128
+ });
129
+ expect(LoggerProxy.error).toHaveBeenCalled();
130
+ });
131
+
132
+ it('preserves top-level trackingId on request failure', async () => {
133
+ const err = {
134
+ statusCode: 503,
135
+ trackingId: 'track-top-level-1',
136
+ message: 'Service unavailable',
137
+ };
138
+ (webex.request as jest.Mock).mockRejectedValue(err);
139
+
140
+ await expect(service.answerCall({callId: 'c1', endpointId: 'e1'})).rejects.toMatchObject({
141
+ isWxAppTelephonyError: true,
142
+ trackingId: 'track-top-level-1',
143
+ status: 503,
144
+ });
145
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
146
+ expect.stringContaining('failed'),
147
+ expect.objectContaining({
148
+ data: expect.objectContaining({trackingId: 'track-top-level-1'}),
149
+ })
150
+ );
151
+ });
152
+ });
153
+
154
+ describe('rejectCall', () => {
155
+ it('POST /reject with callId', async () => {
156
+ await service.rejectCall({callId: 'call-123'});
157
+
158
+ expect(webex.request).toHaveBeenCalledWith({
159
+ uri: `${CALLS_URL}/reject`,
160
+ method: 'POST',
161
+ body: {callId: 'call-123'},
162
+ addAuthHeader: true,
163
+ });
164
+ });
165
+ });
166
+
167
+ describe('muteCall', () => {
168
+ it('POST /mute with callId', async () => {
169
+ await service.muteCall({callId: 'call-123'});
170
+
171
+ expect(webex.request).toHaveBeenCalledWith(
172
+ expect.objectContaining({uri: `${CALLS_URL}/mute`, body: {callId: 'call-123'}})
173
+ );
174
+ });
175
+ });
176
+
177
+ describe('unmuteCall', () => {
178
+ it('POST /unmute with callId', async () => {
179
+ await service.unmuteCall({callId: 'call-123'});
180
+
181
+ expect(webex.request).toHaveBeenCalledWith(
182
+ expect.objectContaining({uri: `${CALLS_URL}/unmute`, body: {callId: 'call-123'}})
183
+ );
184
+ });
185
+ });
186
+
187
+ describe('transmitDtmf', () => {
188
+ it('POST /transmitDtmf with callId and dtmf', async () => {
189
+ await service.transmitDtmf({callId: 'call-123', dtmf: '5'});
190
+
191
+ expect(webex.request).toHaveBeenCalledWith({
192
+ uri: `${CALLS_URL}/transmitDtmf`,
193
+ method: 'POST',
194
+ body: {callId: 'call-123', dtmf: '5'},
195
+ addAuthHeader: true,
196
+ });
197
+ });
198
+
199
+ it('includes lineOwnerId in DTMF request when provided', async () => {
200
+ await service.transmitDtmf({callId: 'c1', dtmf: '9', lineOwnerId: 'lo-1'});
201
+
202
+ expect(webex.request).toHaveBeenCalledWith(
203
+ expect.objectContaining({body: {callId: 'c1', dtmf: '9', lineOwnerId: 'lo-1'}})
204
+ );
205
+ });
206
+ });
207
+
208
+ describe('getCallDetails', () => {
209
+ it('GET /telephony/calls/{callId}', async () => {
210
+ (webex.request as jest.Mock).mockResolvedValue({body: {muted: true}});
211
+
212
+ const result = await service.getCallDetails({callId: 'call-123'});
213
+
214
+ expect(webex.request).toHaveBeenCalledWith({
215
+ uri: `${CALLS_URL}/call-123`,
216
+ method: 'GET',
217
+ addAuthHeader: true,
218
+ });
219
+ expect(result).toEqual({muted: true});
220
+ });
221
+
222
+ it('includes lineOwnerId query param when provided', async () => {
223
+ await service.getCallDetails({callId: 'call-123', lineOwnerId: 'lo-1'});
224
+
225
+ expect(webex.request).toHaveBeenCalledWith(
226
+ expect.objectContaining({uri: `${CALLS_URL}/call-123?lineOwnerId=lo-1`})
227
+ );
228
+ });
229
+
230
+ it('rejects and logs on request failure', async () => {
231
+ (webex.request as jest.Mock).mockRejectedValue(new Error('network fail'));
232
+
233
+ await expect(service.getCallDetails({callId: 'c1'})).rejects.toBeDefined();
234
+ expect(LoggerProxy.error).toHaveBeenCalled();
235
+ });
236
+
237
+ it('logs HTTP status on non-404 getCallDetails failure', async () => {
238
+ const err = {
239
+ statusCode: 503,
240
+ trackingId: 'track-top-level-1',
241
+ message: 'Service unavailable',
242
+ };
243
+ (webex.request as jest.Mock).mockRejectedValue(err);
244
+
245
+ await expect(service.getCallDetails({callId: 'c1'})).rejects.toBeDefined();
246
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
247
+ expect.stringContaining('getCallDetails failed'),
248
+ expect.objectContaining({
249
+ data: expect.objectContaining({
250
+ trackingId: 'track-top-level-1',
251
+ status: 503,
252
+ }),
253
+ })
254
+ );
255
+ });
256
+
257
+ it('rejects without error log for expected call-not-found during mute backfill race', async () => {
258
+ (webex.request as jest.Mock).mockRejectedValue({status: 400, message: 'Call not found'});
259
+
260
+ await expect(service.getCallDetails({callId: 'c1'})).rejects.toBeDefined();
261
+ expect(LoggerProxy.error).not.toHaveBeenCalled();
262
+ });
263
+ });
264
+ });