@webex/contact-center 3.12.0-llmrefactor.11 → 3.12.0-llmrefactor.13

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 (60) hide show
  1. package/.sdd/manifest.json +5 -5
  2. package/ai-docs/CONTRACTS.md +2 -0
  3. package/ai-docs/SERVICE_STATE.md +4 -0
  4. package/ai-docs/contact-center-spec.md +26 -3
  5. package/dist/cc.js +163 -2
  6. package/dist/cc.js.map +1 -1
  7. package/dist/constants.js +5 -1
  8. package/dist/constants.js.map +1 -1
  9. package/dist/index.js +25 -10
  10. package/dist/index.js.map +1 -1
  11. package/dist/metrics/constants.js +5 -0
  12. package/dist/metrics/constants.js.map +1 -1
  13. package/dist/services/ApiAiAssistant.js +116 -1
  14. package/dist/services/ApiAiAssistant.js.map +1 -1
  15. package/dist/services/config/Util.js +5 -1
  16. package/dist/services/config/Util.js.map +1 -1
  17. package/dist/services/config/constants.js +10 -1
  18. package/dist/services/config/constants.js.map +1 -1
  19. package/dist/services/config/index.js +51 -0
  20. package/dist/services/config/index.js.map +1 -1
  21. package/dist/services/config/types.js +4 -3
  22. package/dist/services/config/types.js.map +1 -1
  23. package/dist/services/core/Utils.js +5 -1
  24. package/dist/services/core/Utils.js.map +1 -1
  25. package/dist/types/cc.d.ts +21 -1
  26. package/dist/types/constants.d.ts +4 -0
  27. package/dist/types/index.d.ts +11 -1
  28. package/dist/types/metrics/constants.d.ts +5 -0
  29. package/dist/types/services/ApiAiAssistant.d.ts +35 -2
  30. package/dist/types/services/config/constants.d.ts +5 -0
  31. package/dist/types/services/config/types.d.ts +13 -2
  32. package/dist/types/services/core/Utils.d.ts +2 -0
  33. package/dist/types/types.d.ts +56 -1
  34. package/dist/types.js +39 -2
  35. package/dist/types.js.map +1 -1
  36. package/dist/webex.js +9 -1
  37. package/dist/webex.js.map +1 -1
  38. package/package.json +9 -9
  39. package/src/cc.ts +251 -18
  40. package/src/constants.ts +4 -0
  41. package/src/index.ts +11 -0
  42. package/src/metrics/ai-docs/metrics-spec.md +19 -20
  43. package/src/metrics/constants.ts +5 -0
  44. package/src/services/ApiAiAssistant.ts +153 -1
  45. package/src/services/ai-docs/services-spec.md +11 -3
  46. package/src/services/config/Util.ts +6 -0
  47. package/src/services/config/ai-docs/config-spec.md +14 -3
  48. package/src/services/config/constants.ts +12 -0
  49. package/src/services/config/index.ts +65 -0
  50. package/src/services/config/types.ts +11 -2
  51. package/src/services/core/Utils.ts +4 -0
  52. package/src/types.ts +63 -0
  53. package/src/webex.js +8 -0
  54. package/test/unit/spec/cc.ts +268 -0
  55. package/test/unit/spec/services/ApiAiAssistant.ts +134 -2
  56. package/test/unit/spec/services/config/Util.ts +44 -0
  57. package/test/unit/spec/services/config/index.ts +68 -0
  58. package/test/unit/spec/wellnessSampleUtils.ts +288 -0
  59. package/umd/contact-center.min.js +2 -2
  60. package/umd/contact-center.min.js.map +1 -1
@@ -130,6 +130,7 @@ describe('webex.cc', () => {
130
130
  config: {
131
131
  getAgentConfig: jest.fn(),
132
132
  getOutdialAniEntries: jest.fn(),
133
+ getWellbeingBreakIdleCode: jest.fn(),
133
134
  },
134
135
  webSocketManager: mockWebSocketManager,
135
136
  rtdWebSocketManager: {
@@ -321,6 +322,7 @@ describe('webex.cc', () => {
321
322
  isTimeoutDesktopInactivityEnabled: false,
322
323
  webRtcEnabled: true,
323
324
  lostConnectionRecoveryTimeout: 0,
325
+ isWellnessBreakEnabled: false,
324
326
  };
325
327
 
326
328
  it('should register successfully and return agent profile', async () => {
@@ -496,6 +498,25 @@ describe('webex.cc', () => {
496
498
  );
497
499
  });
498
500
 
501
+ it('clears wellness session context when registration fails after relogin', async () => {
502
+ const mockError = new Error('Error after silent relogin');
503
+ webex.cc.$config = {...webex.cc.$config, allowAutomatedRelogin: true};
504
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue({
505
+ ...mockAgentProfile,
506
+ webRtcEnabled: false,
507
+ aiFeature: undefined,
508
+ } as Profile);
509
+ mockWebSocketManager.initWebSocket.mockResolvedValue({agentId: 'agent123'});
510
+ jest.spyOn(webex.cc as any, 'silentRelogin').mockImplementation(async () => {
511
+ webex.cc['updateWellnessSession']('partial-session');
512
+ throw mockError;
513
+ });
514
+
515
+ await expect(webex.cc.register()).rejects.toThrow(mockError);
516
+
517
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
518
+ });
519
+
499
520
  it('should log error if mercury connect fails but cc.register() should not fail', async () => {
500
521
  const mockError = new Error('Error while performing mercury connect');
501
522
  jest.spyOn(webex.internal.mercury, 'connect').mockRejectedValue(mockError);
@@ -2070,6 +2091,227 @@ describe('webex.cc', () => {
2070
2091
  });
2071
2092
  });
2072
2093
 
2094
+ describe('Agent Wellness Break SDK contracts', () => {
2095
+ const wellbeingCode = {
2096
+ id: 'wellbeing-code',
2097
+ name: 'WellbeingBreak',
2098
+ isSystem: true,
2099
+ isDefault: false,
2100
+ };
2101
+
2102
+ beforeEach(() => {
2103
+ webex.cc.agentConfig = {
2104
+ agentId: 'agent-1',
2105
+ isWellnessBreakEnabled: true,
2106
+ } as Profile;
2107
+ webex.cc['updateWellnessSession']('session-1');
2108
+ });
2109
+
2110
+ it('fetches the system idle code once per registration', async () => {
2111
+ const lookup = jest
2112
+ .spyOn(webex.cc.services.config, 'getWellbeingBreakIdleCode')
2113
+ .mockResolvedValue(wellbeingCode);
2114
+
2115
+ await expect(webex.cc.getWellbeingBreakIdleCode()).resolves.toEqual(wellbeingCode);
2116
+ await expect(webex.cc.getWellbeingBreakIdleCode()).resolves.toEqual(wellbeingCode);
2117
+
2118
+ expect(lookup).toHaveBeenCalledTimes(1);
2119
+ expect(lookup).toHaveBeenCalledWith('mockOrgId');
2120
+ });
2121
+
2122
+ it('does not cache an idle-code lookup that completes after a new registration starts', async () => {
2123
+ let resolveLookup: (idleCode: typeof wellbeingCode) => void = () => undefined;
2124
+ jest
2125
+ .spyOn(webex.cc.services.config, 'getWellbeingBreakIdleCode')
2126
+ .mockImplementation(
2127
+ () =>
2128
+ new Promise((resolve) => {
2129
+ resolveLookup = resolve;
2130
+ })
2131
+ );
2132
+
2133
+ const lookupPromise = webex.cc.getWellbeingBreakIdleCode();
2134
+ const staleLookup = expect(lookupPromise).rejects.toThrow(
2135
+ 'WELLNESS_BREAK_REGISTRATION_CHANGED'
2136
+ );
2137
+ jest
2138
+ .spyOn(webex.cc, 'connectWebsocket')
2139
+ .mockRejectedValue(new Error('replacement registration failed'));
2140
+
2141
+ await expect(webex.cc.register()).rejects.toThrow('replacement registration failed');
2142
+ resolveLookup(wellbeingCode);
2143
+
2144
+ await staleLookup;
2145
+ expect(webex.cc['wellbeingBreakIdleCode']).toBeUndefined();
2146
+ });
2147
+
2148
+ it('rejects the system idle-code lookup when wellness is disabled', async () => {
2149
+ webex.cc.agentConfig.isWellnessBreakEnabled = false;
2150
+
2151
+ await expect(webex.cc.getWellbeingBreakIdleCode()).rejects.toThrow(
2152
+ 'WELLNESS_BREAK_NOT_ENABLED'
2153
+ );
2154
+ expect(webex.cc.services.config.getWellbeingBreakIdleCode).not.toHaveBeenCalled();
2155
+ });
2156
+
2157
+ it('invalidates the wellness session only for a current-session multi-login close', () => {
2158
+ webex.cc['handleWebsocketMessage'](
2159
+ JSON.stringify({
2160
+ type: CC_EVENTS.AGENT_MULTI_LOGIN,
2161
+ data: {
2162
+ type: 'AgentMultiLoginCloseSession',
2163
+ agentSessionId: 'another-session',
2164
+ },
2165
+ })
2166
+ );
2167
+ expect(webex.cc['currentAgentSessionId']).toBe('session-1');
2168
+
2169
+ webex.cc['handleWebsocketMessage'](
2170
+ JSON.stringify({
2171
+ type: CC_EVENTS.AGENT_MULTI_LOGIN,
2172
+ data: {
2173
+ type: 'AgentMultiLoginCloseSession',
2174
+ agentSessionId: 'session-1',
2175
+ },
2176
+ })
2177
+ );
2178
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
2179
+ });
2180
+
2181
+ it('emits primary wellness notifications regardless of notification session metadata', () => {
2182
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2183
+ const payload = {
2184
+ type: 'Wellness_Break_Handler',
2185
+ orgId: 'mockOrgId',
2186
+ trackingId: 'data-notification-tracking',
2187
+ data: {
2188
+ agentId: 'agent-1',
2189
+ orgId: 'mockOrgId',
2190
+ notifType: 'Wellness_Break_Handler',
2191
+ notifDetails: {
2192
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2193
+ actionText: 'How about a break?',
2194
+ },
2195
+ data: {
2196
+ orgId: 'mockOrgId',
2197
+ agentSessionId: 'notification-session',
2198
+ interactionId: 'interaction-1',
2199
+ },
2200
+ },
2201
+ };
2202
+
2203
+ webex.cc['handleWebsocketMessage'](JSON.stringify(payload));
2204
+
2205
+ expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
2206
+ agentId: 'agent-1',
2207
+ orgId: 'mockOrgId',
2208
+ agentSessionId: 'notification-session',
2209
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2210
+ actionText: 'How about a break?',
2211
+ interactionId: 'interaction-1',
2212
+ trackingId: 'data-notification-tracking',
2213
+ });
2214
+ });
2215
+
2216
+ it.each([
2217
+ ['SUGGEST_WELLNESS_BREAK', 'interactionId'],
2218
+ ['WELLNESS_BREAK_NOT_ALLOWED', 'InteractionId'],
2219
+ ])('normalizes the %s action and either interaction-id spelling', (actionEvent, key) => {
2220
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2221
+ webex.cc['handleWebsocketMessage'](
2222
+ JSON.stringify({
2223
+ type: 'Wellness_Break_Handler',
2224
+ orgId: 'mockOrgId',
2225
+ data: {
2226
+ agentId: 'agent-1',
2227
+ orgId: 'mockOrgId',
2228
+ notifType: 'Wellness_Break_Handler',
2229
+ notifDetails: {actionEvent},
2230
+ data: {
2231
+ orgId: 'mockOrgId',
2232
+ agentSessionId: 'session-1',
2233
+ [key]: 'interaction-2',
2234
+ },
2235
+ },
2236
+ })
2237
+ );
2238
+
2239
+ expect(emitSpy).toHaveBeenCalledWith(
2240
+ CC_EVENTS.WELLNESS_BREAK,
2241
+ expect.objectContaining({actionEvent, interactionId: 'interaction-2'})
2242
+ );
2243
+ });
2244
+
2245
+ it('does not promote uncontracted notification text fields', () => {
2246
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2247
+ webex.cc['handleWebsocketMessage'](
2248
+ JSON.stringify({
2249
+ type: 'Wellness_Break_Handler',
2250
+ orgId: 'mockOrgId',
2251
+ trackingId: 'notification-tracking',
2252
+ data: {
2253
+ agentId: 'agent-1',
2254
+ orgId: 'mockOrgId',
2255
+ notifType: 'Wellness_Break_Handler',
2256
+ notifDetails: {
2257
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2258
+ actionText2: 'Uncontracted text',
2259
+ },
2260
+ data: {
2261
+ orgId: 'mockOrgId',
2262
+ agentSessionId: 'session-1',
2263
+ InteractionId: 'interaction-qa',
2264
+ },
2265
+ },
2266
+ })
2267
+ );
2268
+
2269
+ expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
2270
+ agentId: 'agent-1',
2271
+ orgId: 'mockOrgId',
2272
+ agentSessionId: 'session-1',
2273
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2274
+ interactionId: 'interaction-qa',
2275
+ trackingId: 'notification-tracking',
2276
+ });
2277
+ });
2278
+
2279
+ it('ignores wellness events when the effective flag is disabled or identity is missing', () => {
2280
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2281
+ const payload = {
2282
+ type: 'Wellness_Break_Handler',
2283
+ orgId: 'mockOrgId',
2284
+ data: {
2285
+ agentId: 'agent-1',
2286
+ orgId: 'mockOrgId',
2287
+ notifType: 'Wellness_Break_Handler',
2288
+ notifDetails: {actionEvent: 'SUGGEST_WELLNESS_BREAK'},
2289
+ data: {orgId: 'mockOrgId', agentSessionId: 'session-1'},
2290
+ },
2291
+ };
2292
+
2293
+ webex.cc.agentConfig.isWellnessBreakEnabled = false;
2294
+ webex.cc['handleWebsocketMessage'](JSON.stringify(payload));
2295
+ webex.cc.agentConfig.isWellnessBreakEnabled = true;
2296
+ const {orgId: omittedOrgId, ...payloadWithoutEnvelopeOrg} = payload;
2297
+ expect(omittedOrgId).toBe('mockOrgId');
2298
+ webex.cc['handleWebsocketMessage'](JSON.stringify(payloadWithoutEnvelopeOrg));
2299
+
2300
+ expect(emitSpy).not.toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, expect.anything());
2301
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
2302
+ METRIC_EVENT_NAMES.AI_ASSISTANT_WELLNESS_EVENT_INVALID,
2303
+ {reason: 'wellness_disabled'},
2304
+ ['operational']
2305
+ );
2306
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
2307
+ METRIC_EVENT_NAMES.AI_ASSISTANT_WELLNESS_EVENT_INVALID,
2308
+ {reason: 'missing_wellness_identity'},
2309
+ ['operational']
2310
+ );
2311
+ });
2312
+
2313
+ });
2314
+
2073
2315
  describe('getBuddyAgents', () => {
2074
2316
  it('should return buddy agents response when successful', async () => {
2075
2317
  const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
@@ -2701,6 +2943,32 @@ describe('webex.cc', () => {
2701
2943
  deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
2702
2944
  });
2703
2945
 
2946
+ it('invalidates wellness context before deregistration cleanup can fail', async () => {
2947
+ const cleanupError = new Error('cleanup failed');
2948
+ let rejectCleanup: (error: Error) => void = () => undefined;
2949
+ const cleanup = new Promise((resolve, reject) => {
2950
+ rejectCleanup = reject;
2951
+ });
2952
+ webex.cc.agentConfig = {
2953
+ ...webex.cc.agentConfig,
2954
+ isWellnessBreakEnabled: true,
2955
+ };
2956
+ webex.cc['updateWellnessSession']('session-1');
2957
+ jest.spyOn(webex.cc as any, 'teardownWxAppLocalState').mockReturnValue(cleanup);
2958
+
2959
+ const deregistration = webex.cc.deregister();
2960
+ const rejection = expect(deregistration).rejects.toThrow(cleanupError);
2961
+
2962
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
2963
+ await expect(webex.cc.apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
2964
+ 'WELLNESS_BREAK_AGENT_SESSION_REQUIRED'
2965
+ );
2966
+
2967
+ rejectCleanup(cleanupError);
2968
+ await rejection;
2969
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
2970
+ });
2971
+
2704
2972
  it('should unregister successfully and clean up all resources when webrtc is enabled', async () => {
2705
2973
  webex.cc.services.rtdWebSocketManager = {
2706
2974
  isSocketClosed: false,
@@ -2,7 +2,12 @@ import ApiAIAssistant from '../../../../src/services/ApiAiAssistant';
2
2
  import MetricsManager from '../../../../src/metrics/MetricsManager';
3
3
  import LoggerProxy from '../../../../src/logger-proxy';
4
4
  import WebexRequest from '../../../../src/services/core/WebexRequest';
5
- import {HTTP_METHODS, RealTimeAssistanceUserActionId, WebexSDK} from '../../../../src/types';
5
+ import {
6
+ HTTP_METHODS,
7
+ RealTimeAssistanceUserActionId,
8
+ WELLNESS_BREAK_USER_ACTIONS,
9
+ WebexSDK,
10
+ } from '../../../../src/types';
6
11
 
7
12
  jest.mock('../../../../src/metrics/MetricsManager');
8
13
  jest.mock('../../../../src/logger-proxy');
@@ -11,6 +16,11 @@ describe('ApiAIAssistant', () => {
11
16
  let apiAIAssistant: ApiAIAssistant;
12
17
  let mockWebex: WebexSDK;
13
18
  let mockMetricsManager: jest.Mocked<MetricsManager>;
19
+ let wellnessContext: {
20
+ isWellnessBreakEnabled: boolean;
21
+ agentId?: string;
22
+ agentSessionId?: string;
23
+ };
14
24
 
15
25
  beforeEach(() => {
16
26
  jest.clearAllMocks();
@@ -43,7 +53,8 @@ describe('ApiAIAssistant', () => {
43
53
  } as unknown as jest.Mocked<MetricsManager>;
44
54
  (MetricsManager.getInstance as jest.Mock).mockReturnValue(mockMetricsManager);
45
55
 
46
- apiAIAssistant = new ApiAIAssistant(mockWebex);
56
+ wellnessContext = {isWellnessBreakEnabled: false};
57
+ apiAIAssistant = new ApiAIAssistant(mockWebex, () => wellnessContext);
47
58
  });
48
59
 
49
60
  it('should send transcript start event successfully', async () => {
@@ -270,4 +281,125 @@ describe('ApiAIAssistant', () => {
270
281
 
271
282
  expect(errorMessage).toBe('Error while performing sendRealTimeAssistanceUserAction');
272
283
  });
284
+
285
+ describe('Agent Wellness Break actions', () => {
286
+ beforeEach(() => {
287
+ wellnessContext = {
288
+ isWellnessBreakEnabled: true,
289
+ agentId: 'test-agent-id',
290
+ agentSessionId: 'session-1',
291
+ };
292
+ });
293
+
294
+ it('does not expose wellness context mutation on the public assistant instance', () => {
295
+ expect(apiAIAssistant).not.toHaveProperty('setWellnessContext');
296
+ });
297
+
298
+ it('sends the exact REQUESTED body and resolves only for HTTP 202', async () => {
299
+ jest.spyOn(Date, 'now').mockReturnValue(1787640000000);
300
+ (mockWebex.request as jest.Mock).mockResolvedValue({statusCode: 202});
301
+
302
+ await expect(apiAIAssistant.requestWellnessBreak()).resolves.toBeUndefined();
303
+
304
+ expect(mockWebex.request).toHaveBeenCalledWith({
305
+ uri: 'https://api-ai-assistant.produs1.ciscoccservice.com/event',
306
+ method: HTTP_METHODS.POST,
307
+ addAuthHeader: true,
308
+ body: {
309
+ agentId: 'test-agent-id',
310
+ orgId: 'test-org-id',
311
+ eventType: 'CUSTOM_EVENT',
312
+ eventName: 'WellnessBreakAction',
313
+ eventDetails: {
314
+ data: {
315
+ action: 'REQUESTED',
316
+ agentSessionId: 'session-1',
317
+ actionTimeStamp: 1787640000000,
318
+ },
319
+ },
320
+ },
321
+ });
322
+ });
323
+
324
+ it.each([
325
+ WELLNESS_BREAK_USER_ACTIONS.ACCEPTED,
326
+ WELLNESS_BREAK_USER_ACTIONS.REJECTED,
327
+ WELLNESS_BREAK_USER_ACTIONS.NO_RESPONSE,
328
+ ] as const)(
329
+ 'sends the %s offer action',
330
+ async (action) => {
331
+ (mockWebex.request as jest.Mock).mockResolvedValue({statusCode: 202});
332
+
333
+ await apiAIAssistant.respondToWellnessBreak({action});
334
+
335
+ expect(
336
+ (mockWebex.request as jest.Mock).mock.calls[0][0].body.eventDetails.data.action
337
+ ).toBe(action);
338
+ }
339
+ );
340
+
341
+ it('rejects disabled or incomplete internal session context before HTTP', async () => {
342
+ wellnessContext = {
343
+ isWellnessBreakEnabled: false,
344
+ agentId: 'test-agent-id',
345
+ agentSessionId: 'session-1',
346
+ };
347
+ await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
348
+ 'WELLNESS_BREAK_NOT_ENABLED'
349
+ );
350
+
351
+ wellnessContext = {
352
+ isWellnessBreakEnabled: true,
353
+ };
354
+ await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
355
+ 'WELLNESS_BREAK_AGENT_ID_REQUIRED'
356
+ );
357
+
358
+ wellnessContext = {
359
+ isWellnessBreakEnabled: true,
360
+ agentId: 'test-agent-id',
361
+ };
362
+ await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
363
+ 'WELLNESS_BREAK_AGENT_SESSION_REQUIRED'
364
+ );
365
+ expect(mockWebex.request).not.toHaveBeenCalled();
366
+ });
367
+
368
+ it('rejects a missing organization before HTTP', async () => {
369
+ (mockWebex.credentials.getOrgId as jest.Mock).mockReturnValue(' ');
370
+
371
+ await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
372
+ 'WELLNESS_BREAK_ORG_ID_REQUIRED'
373
+ );
374
+ expect(mockWebex.request).not.toHaveBeenCalled();
375
+ });
376
+
377
+ it.each([200, 204, 400, 401, 403, 404, 409, 429, 500])(
378
+ 'rejects the non-202 HTTP status %i',
379
+ async (statusCode) => {
380
+ (mockWebex.request as jest.Mock).mockResolvedValue({statusCode});
381
+
382
+ await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
383
+ `WELLNESS_BREAK_ACTION_UNEXPECTED_STATUS_${statusCode}`
384
+ );
385
+ }
386
+ );
387
+
388
+ it('rejects network failures through the structured error path', async () => {
389
+ (mockWebex.request as jest.Mock).mockRejectedValue(new Error('Network unavailable'));
390
+
391
+ await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
392
+ 'Error while performing requestWellnessBreak'
393
+ );
394
+ });
395
+
396
+ it('rejects unsupported offer actions before HTTP', async () => {
397
+ await expect(
398
+ apiAIAssistant.respondToWellnessBreak({
399
+ action: 'REQUESTED',
400
+ } as unknown as Parameters<ApiAIAssistant['respondToWellnessBreak']>[0])
401
+ ).rejects.toThrow('WELLNESS_BREAK_ACTION_INVALID');
402
+ expect(mockWebex.request).not.toHaveBeenCalled();
403
+ });
404
+ });
273
405
  });
@@ -82,4 +82,48 @@ describe('parseAgentConfigs collaboration access fields', () => {
82
82
  expect(profile.accessEntryPoint).toBe('NONE');
83
83
  expect(profile.accessBuddyTeam).toBe('ALL');
84
84
  });
85
+
86
+ it('enables wellness only when organization config, reminders, and license are present', () => {
87
+ const profile = parseAgentConfigs({
88
+ ...baseProfileData,
89
+ orgSettingsData: {...baseProfileData.orgSettingsData, aiAssistantQuantity: 1},
90
+ aiFeatureFlags: {
91
+ data: [
92
+ {
93
+ id: 'ai-feature-1',
94
+ agentWellbeing: {
95
+ enable: true,
96
+ wellnessBreakReminders: 'ENABLED',
97
+ },
98
+ },
99
+ ],
100
+ },
101
+ });
102
+
103
+ expect(profile.isWellnessBreakEnabled).toBe(true);
104
+ });
105
+
106
+ it.each([
107
+ {quantity: 0, enable: true, reminders: 'ENABLED'},
108
+ {quantity: 1, enable: false, reminders: 'ENABLED'},
109
+ {quantity: 1, enable: true, reminders: 'DISABLED'},
110
+ ] as const)(
111
+ 'disables wellness when an effective gate is missing',
112
+ ({quantity, enable, reminders}) => {
113
+ const profile = parseAgentConfigs({
114
+ ...baseProfileData,
115
+ orgSettingsData: {...baseProfileData.orgSettingsData, aiAssistantQuantity: quantity},
116
+ aiFeatureFlags: {
117
+ data: [
118
+ {
119
+ id: 'ai-feature-1',
120
+ agentWellbeing: {enable, wellnessBreakReminders: reminders},
121
+ },
122
+ ],
123
+ },
124
+ });
125
+
126
+ expect(profile.isWellnessBreakEnabled).toBe(false);
127
+ }
128
+ );
85
129
  });
@@ -705,6 +705,74 @@ describe('AgentConfigService', () => {
705
705
  });
706
706
  });
707
707
 
708
+ describe('getWellbeingBreakIdleCode', () => {
709
+ it('uses the system-code query and finds the exact code across pages', async () => {
710
+ (mockWebexRequest.request as jest.Mock)
711
+ .mockResolvedValueOnce({
712
+ statusCode: 200,
713
+ body: {
714
+ data: [
715
+ {id: 'other', name: 'Other', active: true, isSystemCode: true, defaultCode: false},
716
+ ],
717
+ meta: {totalPages: 2},
718
+ },
719
+ })
720
+ .mockResolvedValueOnce({
721
+ statusCode: 200,
722
+ body: {
723
+ data: [
724
+ {
725
+ id: 'wellbeing-code',
726
+ name: 'WellbeingBreak',
727
+ active: true,
728
+ isSystemCode: true,
729
+ defaultCode: false,
730
+ },
731
+ ],
732
+ meta: {totalPages: 2},
733
+ },
734
+ });
735
+
736
+ await expect(agentConfigService.getWellbeingBreakIdleCode(mockOrgId)).resolves.toEqual({
737
+ id: 'wellbeing-code',
738
+ name: 'WellbeingBreak',
739
+ isSystem: true,
740
+ isDefault: false,
741
+ });
742
+ expect(mockWebexRequest.request).toHaveBeenNthCalledWith(1, {
743
+ service: mockWccAPIURL,
744
+ resource:
745
+ `organization/${mockOrgId}/v2/auxiliary-code?page=0&pageSize=100` +
746
+ '&workType=IDLE_CODE&customFilter=isSystemCode==true&desktopProfileFilter=false',
747
+ method: 'GET',
748
+ });
749
+ expect(mockWebexRequest.request).toHaveBeenNthCalledWith(2, {
750
+ service: mockWccAPIURL,
751
+ resource:
752
+ `organization/${mockOrgId}/v2/auxiliary-code?page=1&pageSize=100` +
753
+ '&workType=IDLE_CODE&customFilter=isSystemCode==true&desktopProfileFilter=false',
754
+ method: 'GET',
755
+ });
756
+ });
757
+
758
+ it('rejects when the exact system code is unavailable', async () => {
759
+ (mockWebexRequest.request as jest.Mock).mockResolvedValue({
760
+ statusCode: 200,
761
+ body: {
762
+ data: [
763
+ {id: 'inactive', name: 'WellbeingBreak', active: false, isSystemCode: true},
764
+ {id: 'lookalike', name: 'wellbeingbreak', active: true, isSystemCode: true},
765
+ ],
766
+ meta: {totalPages: 1},
767
+ },
768
+ });
769
+
770
+ await expect(agentConfigService.getWellbeingBreakIdleCode(mockOrgId)).rejects.toThrow(
771
+ 'WELLBEING_BREAK_IDLE_CODE_NOT_FOUND'
772
+ );
773
+ });
774
+ });
775
+
708
776
  describe('getAgentConfig', () => {
709
777
  const mockTeamData = [
710
778
  {id: 'team1', name: 'Support Team'},