@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.
- package/.sdd/manifest.json +5 -5
- package/ai-docs/CONTRACTS.md +2 -0
- package/ai-docs/SERVICE_STATE.md +4 -0
- package/ai-docs/contact-center-spec.md +26 -3
- package/dist/cc.js +163 -2
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +5 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +25 -10
- package/dist/index.js.map +1 -1
- package/dist/metrics/constants.js +5 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +116 -1
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/config/Util.js +5 -1
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +10 -1
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/index.js +51 -0
- package/dist/services/config/index.js.map +1 -1
- package/dist/services/config/types.js +4 -3
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Utils.js +5 -1
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/types/cc.d.ts +21 -1
- package/dist/types/constants.d.ts +4 -0
- package/dist/types/index.d.ts +11 -1
- package/dist/types/metrics/constants.d.ts +5 -0
- package/dist/types/services/ApiAiAssistant.d.ts +35 -2
- package/dist/types/services/config/constants.d.ts +5 -0
- package/dist/types/services/config/types.d.ts +13 -2
- package/dist/types/services/core/Utils.d.ts +2 -0
- package/dist/types/types.d.ts +56 -1
- package/dist/types.js +39 -2
- package/dist/types.js.map +1 -1
- package/dist/webex.js +9 -1
- package/dist/webex.js.map +1 -1
- package/package.json +9 -9
- package/src/cc.ts +251 -18
- package/src/constants.ts +4 -0
- package/src/index.ts +11 -0
- package/src/metrics/ai-docs/metrics-spec.md +19 -20
- package/src/metrics/constants.ts +5 -0
- package/src/services/ApiAiAssistant.ts +153 -1
- package/src/services/ai-docs/services-spec.md +11 -3
- package/src/services/config/Util.ts +6 -0
- package/src/services/config/ai-docs/config-spec.md +14 -3
- package/src/services/config/constants.ts +12 -0
- package/src/services/config/index.ts +65 -0
- package/src/services/config/types.ts +11 -2
- package/src/services/core/Utils.ts +4 -0
- package/src/types.ts +63 -0
- package/src/webex.js +8 -0
- package/test/unit/spec/cc.ts +268 -0
- package/test/unit/spec/services/ApiAiAssistant.ts +134 -2
- package/test/unit/spec/services/config/Util.ts +44 -0
- package/test/unit/spec/services/config/index.ts +68 -0
- package/test/unit/spec/wellnessSampleUtils.ts +288 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
package/test/unit/spec/cc.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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'},
|