@webex/contact-center 3.12.0-llmrefactor.12 → 3.12.0-llmrefactor.14
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 +6 -5
- package/ai-docs/CONTRACTS.md +8 -0
- package/ai-docs/SERVICE_STATE.md +4 -0
- package/ai-docs/contact-center-spec.md +26 -3
- package/ai-docs/features/contact-owner-change-refactor.md +78 -0
- package/dist/cc.js +202 -9
- 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/behavioral-events.js +6 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +6 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AnswerCallOnWebexService.js +4 -4
- package/dist/services/AnswerCallOnWebexService.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +116 -1
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/WebexCrossClientService.js +5 -2
- package/dist/services/WebexCrossClientService.js.map +1 -1
- package/dist/services/WxAppTelephonyMercurySync.js +6 -3
- package/dist/services/WxAppTelephonyMercurySync.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/services/task/TaskFactory.js +1 -0
- package/dist/services/task/TaskFactory.js.map +1 -1
- package/dist/services/task/TaskManager.js +50 -58
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +63 -44
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
- package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js +52 -11
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
- package/dist/services/wxAppDiagnosticLogging.js +50 -13
- package/dist/services/wxAppDiagnosticLogging.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 +6 -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 +15 -2
- package/dist/types/services/core/Utils.d.ts +2 -0
- package/dist/types/services/task/types.d.ts +2 -0
- package/dist/types/services/task/voice/Voice.d.ts +15 -2
- package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +19 -6
- package/dist/types/services/wxAppDiagnosticLogging.d.ts +12 -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 +294 -25
- package/src/constants.ts +4 -0
- package/src/index.ts +11 -0
- package/src/metrics/ai-docs/metrics-spec.md +61 -20
- package/src/metrics/behavioral-events.ts +6 -0
- package/src/metrics/constants.ts +6 -0
- package/src/services/AnswerCallOnWebexService.ts +4 -4
- package/src/services/ApiAiAssistant.ts +153 -1
- package/src/services/WebexCrossClientService.ts +7 -2
- package/src/services/WxAppTelephonyMercurySync.ts +3 -3
- 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 +13 -2
- package/src/services/core/Utils.ts +4 -0
- package/src/services/task/TaskFactory.ts +1 -0
- package/src/services/task/TaskManager.ts +85 -87
- package/src/services/task/ai-docs/task-spec.md +7 -3
- package/src/services/task/types.ts +16 -0
- package/src/services/task/voice/Voice.ts +74 -52
- package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
- package/src/services/task/voice/wxAppVoiceMethods.ts +83 -24
- package/src/services/wxAppDiagnosticLogging.ts +135 -23
- package/src/types.ts +65 -0
- package/src/webex.js +8 -0
- package/test/unit/spec/cc.ts +469 -19
- package/test/unit/spec/services/ApiAiAssistant.ts +134 -2
- package/test/unit/spec/services/WebexCrossClientService.ts +45 -3
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +1 -1
- package/test/unit/spec/services/config/Util.ts +44 -0
- package/test/unit/spec/services/config/index.ts +68 -0
- package/test/unit/spec/services/task/TaskManager.ts +214 -3
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +33 -0
- package/test/unit/spec/services/task/voice/Voice.ts +10 -51
- package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +121 -5
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +44 -8
- 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
|
@@ -1006,69 +1006,28 @@ describe('Voice Task', () => {
|
|
|
1006
1006
|
});
|
|
1007
1007
|
});
|
|
1008
1008
|
|
|
1009
|
-
describe('wxApp offer
|
|
1010
|
-
|
|
1011
|
-
deviceType: 'wxApp',
|
|
1012
|
-
deviceId: 'device-id-1',
|
|
1013
|
-
deviceCallId: 'call-id-1',
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
const makeWxAppOfferTaskData = () =>
|
|
1017
|
-
createBaseData({
|
|
1018
|
-
agentId: 'agent-1',
|
|
1019
|
-
interaction: {
|
|
1020
|
-
participants: {
|
|
1021
|
-
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1022
|
-
},
|
|
1023
|
-
} as any,
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
it('logs evolving accept reasons through the normal wxApp accept flow', () => {
|
|
1027
|
-
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
1028
|
-
const taskData = makeWxAppOfferTaskData();
|
|
1029
|
-
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1030
|
-
|
|
1031
|
-
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1032
|
-
|
|
1033
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
1034
|
-
expect.objectContaining({acceptReason: 'wxApp_offer_ready'})
|
|
1035
|
-
);
|
|
1036
|
-
|
|
1037
|
-
logSpy.mockClear();
|
|
1038
|
-
voice['setWxAppAcceptInFlight'](true);
|
|
1039
|
-
|
|
1040
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
1041
|
-
expect.objectContaining({acceptReason: 'wxApp_accept_in_flight'})
|
|
1042
|
-
);
|
|
1043
|
-
|
|
1044
|
-
logSpy.mockClear();
|
|
1045
|
-
voice['setWxAppAcceptInFlight'](false);
|
|
1046
|
-
voice['setWxAppAnswerPending'](true);
|
|
1047
|
-
|
|
1048
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
1049
|
-
expect.objectContaining({acceptReason: 'wxApp_answer_pending'})
|
|
1050
|
-
);
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
it('logs browser_webrtc_offer when voice variant is WebRTC on a non-wxApp inbound offer', () => {
|
|
1009
|
+
describe('wxApp offer observability integration', () => {
|
|
1010
|
+
it('delegates OFFERED ui control updates to wxApp offer observability', () => {
|
|
1054
1011
|
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
1055
1012
|
const taskData = createBaseData({
|
|
1056
1013
|
agentId: 'agent-1',
|
|
1057
1014
|
interaction: {
|
|
1058
1015
|
participants: {
|
|
1059
|
-
'agent-1': {
|
|
1016
|
+
'agent-1': {
|
|
1017
|
+
id: 'agent-1',
|
|
1018
|
+
deviceType: 'wxApp',
|
|
1019
|
+
deviceId: 'device-id-1',
|
|
1020
|
+
deviceCallId: 'call-id-1',
|
|
1021
|
+
},
|
|
1060
1022
|
},
|
|
1061
1023
|
} as any,
|
|
1062
1024
|
});
|
|
1063
|
-
const voice = new Voice(dummyContact, taskData, {
|
|
1064
|
-
enableWxBetterTogether: true,
|
|
1065
|
-
voiceVariant: VOICE_VARIANT.WEBRTC,
|
|
1066
|
-
});
|
|
1025
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1067
1026
|
|
|
1068
1027
|
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1069
1028
|
|
|
1070
1029
|
expect(logSpy).toHaveBeenCalledWith(
|
|
1071
|
-
expect.objectContaining({acceptReason: '
|
|
1030
|
+
expect.objectContaining({acceptReason: 'wxApp_offer_ready'})
|
|
1072
1031
|
);
|
|
1073
1032
|
});
|
|
1074
1033
|
});
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import MetricsManager from '../../../../../../src/metrics/MetricsManager';
|
|
2
|
+
import {METRIC_EVENT_NAMES} from '../../../../../../src/metrics/constants';
|
|
3
|
+
import {TaskState} from '../../../../../../src/services/task/state-machine';
|
|
4
|
+
import {TaskData, VOICE_VARIANT} from '../../../../../../src/services/task/types';
|
|
5
|
+
import {
|
|
6
|
+
WxAppOfferObservability,
|
|
7
|
+
WxAppOfferObservabilityContext,
|
|
8
|
+
WXAPP_PARTICIPANT_MISMATCH_GRACE_MS,
|
|
9
|
+
} from '../../../../../../src/services/task/voice/wxAppOfferObservability';
|
|
10
|
+
import {WxAppVoiceDependencies} from '../../../../../../src/services/task/voice/wxAppVoiceMethods';
|
|
11
|
+
import * as wxAppDiagnosticLogging from '../../../../../../src/services/wxAppDiagnosticLogging';
|
|
12
|
+
import {createTaskData} from '../taskTestUtils';
|
|
13
|
+
|
|
14
|
+
const wxAppParticipant = {
|
|
15
|
+
deviceType: 'wxApp',
|
|
16
|
+
deviceId: 'device-id-1',
|
|
17
|
+
deviceCallId: 'call-id-1',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function makeTaskDataWithoutWxAppFields(): TaskData {
|
|
21
|
+
return createTaskData({
|
|
22
|
+
agentId: 'agent-1',
|
|
23
|
+
interaction: {
|
|
24
|
+
participants: {
|
|
25
|
+
'agent-1': {id: 'agent-1'},
|
|
26
|
+
},
|
|
27
|
+
} as TaskData['interaction'],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeTaskDataWithWxAppFields(): TaskData {
|
|
32
|
+
return createTaskData({
|
|
33
|
+
agentId: 'agent-1',
|
|
34
|
+
interaction: {
|
|
35
|
+
participants: {
|
|
36
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
37
|
+
},
|
|
38
|
+
} as TaskData['interaction'],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeMockMetricsManager(): jest.Mocked<Pick<MetricsManager, 'trackEvent'>> {
|
|
43
|
+
return {trackEvent: jest.fn()};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeContext(
|
|
47
|
+
overrides: {
|
|
48
|
+
taskData?: TaskData;
|
|
49
|
+
taskState?: TaskState;
|
|
50
|
+
usersubPublished?: boolean;
|
|
51
|
+
wxAppAcceptInFlight?: boolean;
|
|
52
|
+
wxAppAnswerPending?: boolean;
|
|
53
|
+
voiceVariant?: typeof VOICE_VARIANT.PSTN;
|
|
54
|
+
acceptEnabled?: boolean;
|
|
55
|
+
isOutdial?: boolean;
|
|
56
|
+
} = {}
|
|
57
|
+
): WxAppOfferObservabilityContext & {
|
|
58
|
+
setTaskData: (data: TaskData) => void;
|
|
59
|
+
setTaskState: (s: TaskState) => void;
|
|
60
|
+
setUsersubPublished: (value: boolean) => void;
|
|
61
|
+
} {
|
|
62
|
+
let taskData = overrides.taskData ?? makeTaskDataWithoutWxAppFields();
|
|
63
|
+
let taskState = overrides.taskState ?? TaskState.OFFERED;
|
|
64
|
+
let usersubPublished = overrides.usersubPublished ?? true;
|
|
65
|
+
const metricsManager = makeMockMetricsManager() as unknown as MetricsManager;
|
|
66
|
+
|
|
67
|
+
const deps = (): WxAppVoiceDependencies => ({
|
|
68
|
+
enableWxBetterTogether: true,
|
|
69
|
+
agentId: 'agent-1',
|
|
70
|
+
metricsManager,
|
|
71
|
+
getTaskData: () => taskData,
|
|
72
|
+
getTaskState: () => taskState,
|
|
73
|
+
getWxAppMuted: () => false,
|
|
74
|
+
setWxAppMuted: jest.fn(),
|
|
75
|
+
getUsersubPublished: () => usersubPublished,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
getInteractionId: () => taskData.interactionId,
|
|
80
|
+
getTaskState: () => taskState,
|
|
81
|
+
getAcceptControl: () => ({
|
|
82
|
+
isVisible: true,
|
|
83
|
+
isEnabled: overrides.acceptEnabled ?? false,
|
|
84
|
+
}),
|
|
85
|
+
getVoiceVariant: () => overrides.voiceVariant ?? VOICE_VARIANT.PSTN,
|
|
86
|
+
isOutdial: () => overrides.isOutdial ?? false,
|
|
87
|
+
isWxAppInboundOffer: () => {
|
|
88
|
+
const p = taskData.interaction?.participants?.['agent-1'] as
|
|
89
|
+
| {deviceType?: string; deviceCallId?: string; deviceId?: string}
|
|
90
|
+
| undefined;
|
|
91
|
+
return (
|
|
92
|
+
p?.deviceType === 'wxApp' &&
|
|
93
|
+
typeof p.deviceCallId === 'string' &&
|
|
94
|
+
p.deviceCallId.trim() !== '' &&
|
|
95
|
+
typeof p.deviceId === 'string' &&
|
|
96
|
+
p.deviceId.trim() !== ''
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
isWxAppCallingOffer: () => {
|
|
100
|
+
const p = taskData.interaction?.participants?.['agent-1'] as
|
|
101
|
+
| {deviceType?: string; deviceCallId?: string; deviceId?: string}
|
|
102
|
+
| undefined;
|
|
103
|
+
return (
|
|
104
|
+
p?.deviceType === 'wxApp' &&
|
|
105
|
+
typeof p.deviceCallId === 'string' &&
|
|
106
|
+
p.deviceCallId.trim() !== '' &&
|
|
107
|
+
typeof p.deviceId === 'string' &&
|
|
108
|
+
p.deviceId.trim() !== ''
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
getWxAppAcceptInFlight: () => overrides.wxAppAcceptInFlight ?? false,
|
|
112
|
+
getWxAppAnswerPending: () => overrides.wxAppAnswerPending ?? false,
|
|
113
|
+
getEnableWxBetterTogether: () => true,
|
|
114
|
+
getUsersubPublished: () => usersubPublished,
|
|
115
|
+
getWxAppVoiceDependencies: deps,
|
|
116
|
+
getMetricsManager: () => metricsManager,
|
|
117
|
+
setTaskData: (data: TaskData) => {
|
|
118
|
+
taskData = data;
|
|
119
|
+
},
|
|
120
|
+
setTaskState: (state: TaskState) => {
|
|
121
|
+
taskState = state;
|
|
122
|
+
},
|
|
123
|
+
setUsersubPublished: (value: boolean) => {
|
|
124
|
+
usersubPublished = value;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe('WxAppOfferObservability', () => {
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
jest.useFakeTimers();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
jest.useRealTimers();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('offer decision logging', () => {
|
|
139
|
+
it('logs wxApp_offer_ready when participant fields are present', () => {
|
|
140
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
141
|
+
const observability = new WxAppOfferObservability();
|
|
142
|
+
const ctx = makeContext({taskData: makeTaskDataWithWxAppFields(), acceptEnabled: true});
|
|
143
|
+
|
|
144
|
+
observability.handleUiControlsUpdate(ctx);
|
|
145
|
+
|
|
146
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
147
|
+
expect.objectContaining({acceptReason: 'wxApp_offer_ready'})
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('logs accept reason transitions only once per reason', () => {
|
|
152
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
153
|
+
const observability = new WxAppOfferObservability();
|
|
154
|
+
const ctx = makeContext({
|
|
155
|
+
taskData: makeTaskDataWithWxAppFields(),
|
|
156
|
+
wxAppAcceptInFlight: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
observability.handleUiControlsUpdate(ctx);
|
|
160
|
+
observability.handleUiControlsUpdate(ctx);
|
|
161
|
+
|
|
162
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
164
|
+
expect.objectContaining({acceptReason: 'wxApp_accept_in_flight'})
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('logs browser_webrtc_offer for WebRTC variant without wxApp participant', () => {
|
|
169
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
170
|
+
const observability = new WxAppOfferObservability();
|
|
171
|
+
const ctx = makeContext({
|
|
172
|
+
taskData: createTaskData({
|
|
173
|
+
agentId: 'agent-1',
|
|
174
|
+
interaction: {
|
|
175
|
+
participants: {
|
|
176
|
+
'agent-1': {id: 'agent-1', deviceType: 'phone', deviceId: 'device-id-1'},
|
|
177
|
+
},
|
|
178
|
+
} as TaskData['interaction'],
|
|
179
|
+
}),
|
|
180
|
+
voiceVariant: VOICE_VARIANT.WEBRTC,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
observability.handleUiControlsUpdate(ctx);
|
|
184
|
+
|
|
185
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
186
|
+
expect.objectContaining({acceptReason: 'browser_webrtc_offer'})
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('deferred participant mismatch', () => {
|
|
192
|
+
it('does not warn when wxApp fields arrive before grace period elapses', () => {
|
|
193
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
194
|
+
const observability = new WxAppOfferObservability();
|
|
195
|
+
const ctx = makeContext();
|
|
196
|
+
|
|
197
|
+
observability.handleUiControlsUpdate(ctx);
|
|
198
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
199
|
+
|
|
200
|
+
ctx.setTaskData(makeTaskDataWithWxAppFields());
|
|
201
|
+
observability.handleUiControlsUpdate(ctx);
|
|
202
|
+
|
|
203
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
204
|
+
|
|
205
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
206
|
+
expect(ctx.getMetricsManager().trackEvent).not.toHaveBeenCalledWith(
|
|
207
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
208
|
+
expect.anything(),
|
|
209
|
+
expect.anything()
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('warns and emits metric when fields remain missing after grace period', () => {
|
|
214
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
215
|
+
const observability = new WxAppOfferObservability();
|
|
216
|
+
const ctx = makeContext();
|
|
217
|
+
|
|
218
|
+
observability.handleUiControlsUpdate(ctx);
|
|
219
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
220
|
+
|
|
221
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
222
|
+
|
|
223
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
224
|
+
expect.objectContaining({
|
|
225
|
+
interactionId: 'interaction-1',
|
|
226
|
+
usersubPublished: true,
|
|
227
|
+
hasDeviceCallId: false,
|
|
228
|
+
hasDeviceId: false,
|
|
229
|
+
acceptReason: 'extension_non_wxApp_offer',
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
expect(ctx.getMetricsManager().trackEvent).toHaveBeenCalledWith(
|
|
233
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
234
|
+
expect.objectContaining({
|
|
235
|
+
taskId: 'interaction-1',
|
|
236
|
+
usersubPublished: true,
|
|
237
|
+
hasDeviceCallId: false,
|
|
238
|
+
hasDeviceId: false,
|
|
239
|
+
acceptReason: 'extension_non_wxApp_offer',
|
|
240
|
+
}),
|
|
241
|
+
['operational', 'behavioral']
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('cancels pending mismatch when task leaves OFFERED before grace period', () => {
|
|
246
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
247
|
+
const observability = new WxAppOfferObservability();
|
|
248
|
+
const ctx = makeContext();
|
|
249
|
+
|
|
250
|
+
observability.handleUiControlsUpdate(ctx);
|
|
251
|
+
ctx.setTaskState(TaskState.TERMINATED);
|
|
252
|
+
observability.handleUiControlsUpdate(ctx);
|
|
253
|
+
|
|
254
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
255
|
+
|
|
256
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('clears grace timer on dispose', () => {
|
|
260
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
261
|
+
const observability = new WxAppOfferObservability();
|
|
262
|
+
const ctx = makeContext();
|
|
263
|
+
|
|
264
|
+
observability.handleUiControlsUpdate(ctx);
|
|
265
|
+
observability.dispose();
|
|
266
|
+
|
|
267
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
268
|
+
|
|
269
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('does not reschedule diagnostics after dispose', () => {
|
|
273
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
274
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
275
|
+
const observability = new WxAppOfferObservability();
|
|
276
|
+
const ctx = makeContext();
|
|
277
|
+
|
|
278
|
+
observability.dispose();
|
|
279
|
+
observability.handleUiControlsUpdate(ctx);
|
|
280
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
281
|
+
|
|
282
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
283
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
284
|
+
expect(ctx.getMetricsManager().trackEvent).not.toHaveBeenCalledWith(
|
|
285
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
286
|
+
expect.anything(),
|
|
287
|
+
expect.anything()
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('does not warn or emit metric for outdial offers with missing wxApp fields', () => {
|
|
292
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
293
|
+
const observability = new WxAppOfferObservability();
|
|
294
|
+
const ctx = makeContext({isOutdial: true});
|
|
295
|
+
|
|
296
|
+
observability.handleUiControlsUpdate(ctx);
|
|
297
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
298
|
+
|
|
299
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
300
|
+
expect(ctx.getMetricsManager().trackEvent).not.toHaveBeenCalledWith(
|
|
301
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
302
|
+
expect.anything(),
|
|
303
|
+
expect.anything()
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('does not schedule mismatch while usersub is unpublished, then emits after refresh when usersub becomes active', () => {
|
|
308
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
309
|
+
const observability = new WxAppOfferObservability();
|
|
310
|
+
const ctx = makeContext({usersubPublished: false});
|
|
311
|
+
|
|
312
|
+
observability.handleUiControlsUpdate(ctx);
|
|
313
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
314
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
315
|
+
|
|
316
|
+
ctx.setUsersubPublished(true);
|
|
317
|
+
observability.handleUiControlsUpdate(ctx);
|
|
318
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
319
|
+
|
|
320
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
321
|
+
|
|
322
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
323
|
+
expect.objectContaining({
|
|
324
|
+
usersubPublished: true,
|
|
325
|
+
hasDeviceCallId: false,
|
|
326
|
+
hasDeviceId: false,
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
expect(ctx.getMetricsManager().trackEvent).toHaveBeenCalledWith(
|
|
330
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
331
|
+
expect.objectContaining({
|
|
332
|
+
usersubPublished: true,
|
|
333
|
+
hasDeviceCallId: false,
|
|
334
|
+
hasDeviceId: false,
|
|
335
|
+
}),
|
|
336
|
+
['operational', 'behavioral']
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('does not emit mismatch after usersub refresh when participant fields are already valid', () => {
|
|
341
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
342
|
+
const observability = new WxAppOfferObservability();
|
|
343
|
+
const ctx = makeContext({
|
|
344
|
+
taskData: makeTaskDataWithWxAppFields(),
|
|
345
|
+
usersubPublished: false,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
observability.handleUiControlsUpdate(ctx);
|
|
349
|
+
ctx.setUsersubPublished(true);
|
|
350
|
+
observability.handleUiControlsUpdate(ctx);
|
|
351
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
352
|
+
|
|
353
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
354
|
+
expect(ctx.getMetricsManager().trackEvent).not.toHaveBeenCalledWith(
|
|
355
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
356
|
+
expect.anything(),
|
|
357
|
+
expect.anything()
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -431,7 +431,12 @@ describe('runWxAppAccept', () => {
|
|
|
431
431
|
]);
|
|
432
432
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
433
433
|
METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS,
|
|
434
|
-
{
|
|
434
|
+
expect.objectContaining({
|
|
435
|
+
taskId: 'interaction-1',
|
|
436
|
+
acceptReason: 'wxApp_offer_ready',
|
|
437
|
+
hasDeviceCallId: true,
|
|
438
|
+
hasDeviceId: true,
|
|
439
|
+
}),
|
|
435
440
|
['operational', 'behavioral']
|
|
436
441
|
);
|
|
437
442
|
expect(lifecycle.setWxAppAcceptInFlight).toHaveBeenNthCalledWith(1, true);
|
|
@@ -455,6 +460,7 @@ describe('runWxAppAccept', () => {
|
|
|
455
460
|
expect.objectContaining({
|
|
456
461
|
taskId: 'interaction-1',
|
|
457
462
|
trackingId: 'track-1',
|
|
463
|
+
acceptReason: 'wxApp_offer_ready',
|
|
458
464
|
}),
|
|
459
465
|
['operational', 'behavioral']
|
|
460
466
|
);
|
|
@@ -462,6 +468,42 @@ describe('runWxAppAccept', () => {
|
|
|
462
468
|
expect(lifecycle.mapWxAppVoiceError).toHaveBeenCalledWith(error, METHODS.ACCEPT);
|
|
463
469
|
expect(lifecycle.setWxAppAcceptInFlight).toHaveBeenLastCalledWith(false);
|
|
464
470
|
});
|
|
471
|
+
|
|
472
|
+
it('includes captured acceptReason on accept success when pending', async () => {
|
|
473
|
+
const deps = makeDeps();
|
|
474
|
+
const lifecycle = makeLifecycle();
|
|
475
|
+
|
|
476
|
+
await runWxAppAccept(deps, lifecycle, {acceptReason: 'wxApp_answer_pending'});
|
|
477
|
+
|
|
478
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
479
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS,
|
|
480
|
+
expect.objectContaining({
|
|
481
|
+
taskId: 'interaction-1',
|
|
482
|
+
acceptReason: 'wxApp_answer_pending',
|
|
483
|
+
}),
|
|
484
|
+
['operational', 'behavioral']
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('includes captured acceptReason on accept failure when pending', async () => {
|
|
489
|
+
const deps = makeDeps();
|
|
490
|
+
const error = new Error('answer failed');
|
|
491
|
+
deps.answerCallOnWebexService!.answerCall = jest.fn().mockRejectedValue(error);
|
|
492
|
+
const lifecycle = makeLifecycle();
|
|
493
|
+
|
|
494
|
+
await expect(
|
|
495
|
+
runWxAppAccept(deps, lifecycle, {acceptReason: 'wxApp_answer_pending'})
|
|
496
|
+
).rejects.toThrow('answer failed');
|
|
497
|
+
|
|
498
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
499
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_FAILED,
|
|
500
|
+
expect.objectContaining({
|
|
501
|
+
taskId: 'interaction-1',
|
|
502
|
+
acceptReason: 'wxApp_answer_pending',
|
|
503
|
+
}),
|
|
504
|
+
['operational', 'behavioral']
|
|
505
|
+
);
|
|
506
|
+
});
|
|
465
507
|
});
|
|
466
508
|
|
|
467
509
|
describe('runWxAppReject', () => {
|
|
@@ -473,7 +515,12 @@ describe('runWxAppReject', () => {
|
|
|
473
515
|
|
|
474
516
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
475
517
|
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
|
|
476
|
-
{
|
|
518
|
+
expect.objectContaining({
|
|
519
|
+
taskId: 'interaction-1',
|
|
520
|
+
acceptReason: 'wxApp_offer_ready',
|
|
521
|
+
hasDeviceCallId: true,
|
|
522
|
+
hasDeviceId: true,
|
|
523
|
+
}),
|
|
477
524
|
['operational', 'behavioral']
|
|
478
525
|
);
|
|
479
526
|
});
|
|
@@ -488,11 +535,50 @@ describe('runWxAppReject', () => {
|
|
|
488
535
|
|
|
489
536
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
490
537
|
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
|
|
491
|
-
expect.objectContaining({
|
|
538
|
+
expect.objectContaining({
|
|
539
|
+
taskId: 'interaction-1',
|
|
540
|
+
acceptReason: 'wxApp_offer_ready',
|
|
541
|
+
}),
|
|
492
542
|
['operational', 'behavioral']
|
|
493
543
|
);
|
|
494
544
|
expect(lifecycle.mapWxAppVoiceError).toHaveBeenCalledWith(error, METHODS.REJECT);
|
|
495
545
|
});
|
|
546
|
+
|
|
547
|
+
it('includes captured acceptReason on decline success when pending', async () => {
|
|
548
|
+
const deps = makeDeps();
|
|
549
|
+
const lifecycle = makeLifecycle();
|
|
550
|
+
|
|
551
|
+
await runWxAppReject(deps, lifecycle, {acceptReason: 'wxApp_answer_pending'});
|
|
552
|
+
|
|
553
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
554
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
|
|
555
|
+
expect.objectContaining({
|
|
556
|
+
taskId: 'interaction-1',
|
|
557
|
+
acceptReason: 'wxApp_answer_pending',
|
|
558
|
+
}),
|
|
559
|
+
['operational', 'behavioral']
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('includes captured acceptReason on decline failure when pending', async () => {
|
|
564
|
+
const deps = makeDeps();
|
|
565
|
+
const error = new Error('reject failed');
|
|
566
|
+
deps.answerCallOnWebexService!.rejectCall = jest.fn().mockRejectedValue(error);
|
|
567
|
+
const lifecycle = makeLifecycle();
|
|
568
|
+
|
|
569
|
+
await expect(
|
|
570
|
+
runWxAppReject(deps, lifecycle, {acceptReason: 'wxApp_answer_pending'})
|
|
571
|
+
).rejects.toThrow('reject failed');
|
|
572
|
+
|
|
573
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
574
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
|
|
575
|
+
expect.objectContaining({
|
|
576
|
+
taskId: 'interaction-1',
|
|
577
|
+
acceptReason: 'wxApp_answer_pending',
|
|
578
|
+
}),
|
|
579
|
+
['operational', 'behavioral']
|
|
580
|
+
);
|
|
581
|
+
});
|
|
496
582
|
});
|
|
497
583
|
|
|
498
584
|
describe('runWxAppOutdialDecline', () => {
|
|
@@ -509,7 +595,30 @@ describe('runWxAppOutdialDecline', () => {
|
|
|
509
595
|
]);
|
|
510
596
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
511
597
|
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
|
|
512
|
-
{
|
|
598
|
+
expect.objectContaining({
|
|
599
|
+
taskId: 'interaction-1',
|
|
600
|
+
acceptReason: 'wxApp_offer_ready',
|
|
601
|
+
hasDeviceCallId: true,
|
|
602
|
+
hasDeviceId: true,
|
|
603
|
+
}),
|
|
604
|
+
['operational', 'behavioral']
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('tracks post-accept acceptReason on outdial cancel when provided', async () => {
|
|
609
|
+
const deps = makeDeps();
|
|
610
|
+
const cancelResult = {type: 'RoutingMessage', trackingId: 'track-1', data: {}};
|
|
611
|
+
|
|
612
|
+
await runWxAppOutdialDecline(deps, async () => cancelResult, {
|
|
613
|
+
acceptReason: 'wxApp_answer_pending',
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
617
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
|
|
618
|
+
expect.objectContaining({
|
|
619
|
+
taskId: 'interaction-1',
|
|
620
|
+
acceptReason: 'wxApp_answer_pending',
|
|
621
|
+
}),
|
|
513
622
|
['operational', 'behavioral']
|
|
514
623
|
);
|
|
515
624
|
});
|
|
@@ -555,6 +664,8 @@ describe('runWxAppOutdialDecline', () => {
|
|
|
555
664
|
trackingId: 'aqm-track-1',
|
|
556
665
|
failureReason: 'TASK_NOT_FOUND',
|
|
557
666
|
reasonCode: 404,
|
|
667
|
+
hasDeviceCallId: true,
|
|
668
|
+
hasDeviceId: true,
|
|
558
669
|
}),
|
|
559
670
|
['operational', 'behavioral']
|
|
560
671
|
);
|
|
@@ -596,7 +707,12 @@ describe('runWxAppTransmitDtmf metrics', () => {
|
|
|
596
707
|
|
|
597
708
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
598
709
|
METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_SUCCESS,
|
|
599
|
-
{
|
|
710
|
+
expect.objectContaining({
|
|
711
|
+
taskId: 'interaction-1',
|
|
712
|
+
dtmfLength: 3,
|
|
713
|
+
hasDeviceCallId: true,
|
|
714
|
+
hasDeviceId: true,
|
|
715
|
+
}),
|
|
600
716
|
['operational', 'behavioral']
|
|
601
717
|
);
|
|
602
718
|
});
|