@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
|
@@ -3,13 +3,15 @@ import {
|
|
|
3
3
|
callIdSuffix,
|
|
4
4
|
deriveWxAppAcceptReason,
|
|
5
5
|
logWxAppOfferDecision,
|
|
6
|
+
logWxAppOfferParticipantMismatch,
|
|
6
7
|
logWxAppSessionReadiness,
|
|
7
8
|
logWxAppTelephonyAction,
|
|
8
9
|
WXAPP_LOG_PREFIX,
|
|
9
10
|
} from '../../../../src/services/wxAppDiagnosticLogging';
|
|
10
11
|
|
|
11
12
|
jest.mock('../../../../src/logger-proxy', () => ({
|
|
12
|
-
|
|
13
|
+
log: jest.fn(),
|
|
14
|
+
warn: jest.fn(),
|
|
13
15
|
error: jest.fn(),
|
|
14
16
|
}));
|
|
15
17
|
|
|
@@ -76,7 +78,7 @@ describe('wxAppDiagnosticLogging', () => {
|
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
describe('log helpers', () => {
|
|
79
|
-
it('logs session readiness with
|
|
81
|
+
it('logs session readiness at log level with grep-friendly suffix', () => {
|
|
80
82
|
logWxAppSessionReadiness({
|
|
81
83
|
enableWxBetterTogether: true,
|
|
82
84
|
loginOption: 'EXTENSION',
|
|
@@ -86,8 +88,8 @@ describe('wxAppDiagnosticLogging', () => {
|
|
|
86
88
|
telephonyTaskType: 'Voice',
|
|
87
89
|
});
|
|
88
90
|
|
|
89
|
-
expect(LoggerProxy.
|
|
90
|
-
`${WXAPP_LOG_PREFIX} session readiness
|
|
91
|
+
expect(LoggerProxy.log).toHaveBeenCalledWith(
|
|
92
|
+
expect.stringContaining(`${WXAPP_LOG_PREFIX} session readiness`),
|
|
91
93
|
expect.objectContaining({
|
|
92
94
|
module: 'wxAppDiagnosticLogging',
|
|
93
95
|
method: 'logWxAppSessionReadiness',
|
|
@@ -95,27 +97,61 @@ describe('wxAppDiagnosticLogging', () => {
|
|
|
95
97
|
feature: 'wxApp',
|
|
96
98
|
event: 'session_readiness',
|
|
97
99
|
loginOption: 'EXTENSION',
|
|
100
|
+
usersubPublished: true,
|
|
101
|
+
mercurySubscribed: true,
|
|
98
102
|
}),
|
|
99
103
|
})
|
|
100
104
|
);
|
|
105
|
+
expect(LoggerProxy.log).toHaveBeenCalledWith(
|
|
106
|
+
expect.stringContaining('usersubPublished=true'),
|
|
107
|
+
expect.anything()
|
|
108
|
+
);
|
|
101
109
|
});
|
|
102
110
|
|
|
103
|
-
it('logs offer decision with acceptReason', () => {
|
|
111
|
+
it('logs offer decision at log level with acceptReason suffix', () => {
|
|
104
112
|
logWxAppOfferDecision({
|
|
105
113
|
interactionId: 'task-1',
|
|
106
114
|
acceptVisible: true,
|
|
107
115
|
acceptEnabled: false,
|
|
108
116
|
acceptReason: 'extension_non_wxApp_offer',
|
|
109
117
|
hasDeviceCallId: false,
|
|
118
|
+
hasDeviceId: false,
|
|
119
|
+
usersubPublished: true,
|
|
110
120
|
});
|
|
111
121
|
|
|
112
|
-
expect(LoggerProxy.
|
|
113
|
-
|
|
122
|
+
expect(LoggerProxy.log).toHaveBeenCalledWith(
|
|
123
|
+
expect.stringContaining('acceptReason=extension_non_wxApp_offer'),
|
|
114
124
|
expect.objectContaining({
|
|
115
125
|
data: expect.objectContaining({
|
|
116
126
|
event: 'offer_decision',
|
|
117
127
|
acceptReason: 'extension_non_wxApp_offer',
|
|
118
128
|
interactionId: 'task-1',
|
|
129
|
+
usersubPublished: true,
|
|
130
|
+
}),
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('logs participant mismatch at warn level', () => {
|
|
136
|
+
logWxAppOfferParticipantMismatch({
|
|
137
|
+
interactionId: 'task-1',
|
|
138
|
+
usersubPublished: true,
|
|
139
|
+
hasDeviceCallId: false,
|
|
140
|
+
hasDeviceId: false,
|
|
141
|
+
participantDeviceType: 'wxApp',
|
|
142
|
+
acceptVisible: true,
|
|
143
|
+
acceptEnabled: false,
|
|
144
|
+
acceptReason: 'extension_non_wxApp_offer',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(LoggerProxy.warn).toHaveBeenCalledWith(
|
|
148
|
+
expect.stringContaining('participant fields mismatch'),
|
|
149
|
+
expect.objectContaining({
|
|
150
|
+
method: 'logWxAppOfferParticipantMismatch',
|
|
151
|
+
data: expect.objectContaining({
|
|
152
|
+
event: 'participant_fields_mismatch',
|
|
153
|
+
usersubPublished: true,
|
|
154
|
+
hasDeviceCallId: false,
|
|
119
155
|
}),
|
|
120
156
|
})
|
|
121
157
|
);
|
|
@@ -131,7 +167,7 @@ describe('wxAppDiagnosticLogging', () => {
|
|
|
131
167
|
});
|
|
132
168
|
|
|
133
169
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
134
|
-
|
|
170
|
+
expect.stringContaining('telephony accept failed'),
|
|
135
171
|
expect.objectContaining({
|
|
136
172
|
data: expect.objectContaining({
|
|
137
173
|
trackingId: 'track-abc',
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const {
|
|
2
|
+
areAllTasksSafe,
|
|
3
|
+
createRecoveryMarker,
|
|
4
|
+
getLegacyExternalTransitionDecision,
|
|
5
|
+
getRecoveryDecision,
|
|
6
|
+
getSelectableIdleCodes,
|
|
7
|
+
getWellnessTransition,
|
|
8
|
+
parseRecoveryMarker,
|
|
9
|
+
shouldResetForSessionEvent,
|
|
10
|
+
shouldResetSampleAfterDeregister,
|
|
11
|
+
} = require('../../../../../../docs/samples/contact-center/wellness-utils');
|
|
12
|
+
const {readFileSync} = require('fs');
|
|
13
|
+
const {resolve} = require('path');
|
|
14
|
+
|
|
15
|
+
describe('Contact Center wellness sample utilities', () => {
|
|
16
|
+
describe('safe task detection', () => {
|
|
17
|
+
it.each([
|
|
18
|
+
['new', false, true],
|
|
19
|
+
['reserved', false, true],
|
|
20
|
+
['connected', false, true],
|
|
21
|
+
['consulting', false, true],
|
|
22
|
+
['conference', false, true],
|
|
23
|
+
['wrapup', false, true],
|
|
24
|
+
['campaignPreview', false, true],
|
|
25
|
+
['terminated', false, false],
|
|
26
|
+
['completed', false, false],
|
|
27
|
+
['completed', true, true],
|
|
28
|
+
])(
|
|
29
|
+
'classifies task state %s with wrapUpRequired=%s',
|
|
30
|
+
(state, wrapUpRequired, expectedBlocking) => {
|
|
31
|
+
const taskList = {
|
|
32
|
+
task: {
|
|
33
|
+
data: {
|
|
34
|
+
wrapUpRequired,
|
|
35
|
+
interaction: {state},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
expect(areAllTasksSafe(taskList)).toBe(!expectedBlocking);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
it('requires every task to be terminal', () => {
|
|
45
|
+
expect(
|
|
46
|
+
areAllTasksSafe({
|
|
47
|
+
completed: {data: {interaction: {state: 'completed'}}},
|
|
48
|
+
active: {data: {interaction: {state: 'engaged'}}},
|
|
49
|
+
})
|
|
50
|
+
).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('treats an incomplete task shape as blocking until authoritative state arrives', () => {
|
|
54
|
+
expect(areAllTasksSafe({task: {}})).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('external state synchronization', () => {
|
|
59
|
+
it.each([
|
|
60
|
+
['OnBreak', 'user-idle', 'complete'],
|
|
61
|
+
['OnBreak', 'rona-code', 'ignore'],
|
|
62
|
+
['WaitingForSafeState', 'rona-code', 'cancel'],
|
|
63
|
+
['Ready', '0', 'ignore'],
|
|
64
|
+
])(
|
|
65
|
+
'returns %s / %s as %s',
|
|
66
|
+
(lifecycle, nextAuxCodeId, expected) => {
|
|
67
|
+
expect(
|
|
68
|
+
getLegacyExternalTransitionDecision({
|
|
69
|
+
lifecycle,
|
|
70
|
+
nextAuxCodeId,
|
|
71
|
+
wellnessAuxCodeId: 'wellness',
|
|
72
|
+
validIdleCodeIds: ['user-idle'],
|
|
73
|
+
})
|
|
74
|
+
).toBe(expected);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('ordinary status selector', () => {
|
|
80
|
+
it('excludes WellbeingBreak and every other system code', () => {
|
|
81
|
+
expect(
|
|
82
|
+
getSelectableIdleCodes([
|
|
83
|
+
{id: '0', name: 'Available', isSystem: false},
|
|
84
|
+
{id: 'training', name: 'Training', isSystem: false},
|
|
85
|
+
{id: 'wellness', name: 'WellbeingBreak', isSystem: true},
|
|
86
|
+
{id: 'rona', name: 'RONA', isSystem: true},
|
|
87
|
+
])
|
|
88
|
+
).toEqual([
|
|
89
|
+
{id: '0', name: 'Available', isSystem: false},
|
|
90
|
+
{id: 'training', name: 'Training', isSystem: false},
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('sample public surface guard', () => {
|
|
96
|
+
it('attaches the wellness constants consumed by the standalone browser sample', () => {
|
|
97
|
+
const webexSource = readFileSync(
|
|
98
|
+
resolve(__dirname, '../../../src/webex.js'),
|
|
99
|
+
'utf8'
|
|
100
|
+
);
|
|
101
|
+
const appSource = readFileSync(
|
|
102
|
+
resolve(__dirname, '../../../../../../docs/samples/contact-center/app.js'),
|
|
103
|
+
'utf8'
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
['WELLNESS_BREAK_NOTIFICATION_ACTIONS', 'WELLNESS_BREAK_USER_ACTIONS'].forEach(
|
|
107
|
+
(surface) => {
|
|
108
|
+
expect(webexSource).toContain(`Webex.${surface} = ${surface};`);
|
|
109
|
+
expect(appSource).toContain(surface);
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('keeps the intake v0.4 US English wellness copy in the sample surfaces', () => {
|
|
115
|
+
const appSource = readFileSync(
|
|
116
|
+
resolve(__dirname, '../../../../../../docs/samples/contact-center/app.js'),
|
|
117
|
+
'utf8'
|
|
118
|
+
);
|
|
119
|
+
const htmlSource = readFileSync(
|
|
120
|
+
resolve(__dirname, '../../../../../../docs/samples/contact-center/index.html'),
|
|
121
|
+
'utf8'
|
|
122
|
+
);
|
|
123
|
+
const sampleSource = `${appSource}\n${htmlSource}`.replace(/\\'/g, "'");
|
|
124
|
+
|
|
125
|
+
[
|
|
126
|
+
'Well-being break scheduled',
|
|
127
|
+
'This break is pre-approved by your organization for your well-being. You deserve it.',
|
|
128
|
+
'Take wellbeing break',
|
|
129
|
+
'Take a break',
|
|
130
|
+
'Later',
|
|
131
|
+
'Great. Your well-being break will begin shortly.',
|
|
132
|
+
'Great. Your well-being break starts right after this call.',
|
|
133
|
+
"It's great to see your dedication. But remember, taking breaks can boost your productivity and your health.",
|
|
134
|
+
"I'm sorry, you've reached your well-being break limit today. Continue with your tasks, but remember to take care of yourself.",
|
|
135
|
+
"Looks like you're busy. I didn't get a response, so I'll check back with you shortly.",
|
|
136
|
+
'Relax',
|
|
137
|
+
'Your 1 minute well-being break is starting in',
|
|
138
|
+
'This moment is yours.',
|
|
139
|
+
"In a few moments, you'll return to your day.",
|
|
140
|
+
'Transitioning back to work mode in',
|
|
141
|
+
'Well-being break completed',
|
|
142
|
+
"I hope you're feeling recharged after that well-being break. See you in your next break!",
|
|
143
|
+
"We couldn't start your well-being break due to a system issue. Please continue with your tasks and we will see you in your next well-being break.",
|
|
144
|
+
'We encountered an issue setting your status to Available.',
|
|
145
|
+
"We couldn't start your well-being break due to a system issue.",
|
|
146
|
+
].forEach((copy) => expect(sampleSource).toContain(copy));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('lifecycle transitions', () => {
|
|
152
|
+
it('revokes manual request eligibility after a not-allowed decision', () => {
|
|
153
|
+
expect(
|
|
154
|
+
getWellnessTransition(
|
|
155
|
+
{lifecycle: 'RequestPending', hasOffer: false, isReady: true},
|
|
156
|
+
{type: 'NOT_ALLOWED'}
|
|
157
|
+
)
|
|
158
|
+
).toEqual({
|
|
159
|
+
lifecycle: 'Ready',
|
|
160
|
+
canRequest: false,
|
|
161
|
+
clearOffer: true,
|
|
162
|
+
clearManualRequest: true,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('keeps an offer retryable when response delivery fails', () => {
|
|
167
|
+
expect(
|
|
168
|
+
getWellnessTransition(
|
|
169
|
+
{lifecycle: 'OfferPending', hasOffer: true, isReady: true},
|
|
170
|
+
{type: 'OFFER_RESPONSE_STARTED'}
|
|
171
|
+
)
|
|
172
|
+
).toEqual({lifecycle: 'OfferResponding', allowed: true});
|
|
173
|
+
expect(
|
|
174
|
+
getWellnessTransition(
|
|
175
|
+
{lifecycle: 'OfferResponding', hasOffer: true, isReady: true},
|
|
176
|
+
{type: 'OFFER_RESPONSE_FAILED'}
|
|
177
|
+
)
|
|
178
|
+
).toEqual({lifecycle: 'OfferPending', rearmOffer: true});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('clears an offer only after its response succeeds', () => {
|
|
182
|
+
expect(
|
|
183
|
+
getWellnessTransition(
|
|
184
|
+
{lifecycle: 'OfferResponding', hasOffer: true, isReady: true},
|
|
185
|
+
{type: 'OFFER_RESPONSE_SUCCEEDED'}
|
|
186
|
+
)
|
|
187
|
+
).toEqual({lifecycle: 'Ready', clearOffer: true});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it.each([
|
|
191
|
+
[undefined, 'session-1', true],
|
|
192
|
+
['session-1', 'session-1', true],
|
|
193
|
+
['session-2', 'session-1', false],
|
|
194
|
+
])(
|
|
195
|
+
'scopes session cleanup for event session %s',
|
|
196
|
+
(eventSessionId, activeSessionId, expected) => {
|
|
197
|
+
expect(shouldResetForSessionEvent(eventSessionId, activeSessionId)).toBe(expected);
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
it.each([
|
|
202
|
+
[true, true, true],
|
|
203
|
+
[false, false, true],
|
|
204
|
+
[false, true, false],
|
|
205
|
+
])(
|
|
206
|
+
'decides deregistration cleanup for success=%s config=%s',
|
|
207
|
+
(succeeded, hasAgentConfig, expected) => {
|
|
208
|
+
expect(shouldResetSampleAfterDeregister({succeeded, hasAgentConfig})).toBe(expected);
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('browser recovery marker', () => {
|
|
214
|
+
it('round-trips only legacy session ownership', () => {
|
|
215
|
+
const marker = createRecoveryMarker({
|
|
216
|
+
agentSessionId: 'session-1',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(parseRecoveryMarker(JSON.stringify(marker))).toEqual({
|
|
220
|
+
version: 1,
|
|
221
|
+
agentSessionId: 'session-1',
|
|
222
|
+
stateModel: 'legacy',
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it.each([
|
|
227
|
+
{
|
|
228
|
+
name: 'discards another session',
|
|
229
|
+
input: {
|
|
230
|
+
marker: createRecoveryMarker({
|
|
231
|
+
agentSessionId: 'session-1',
|
|
232
|
+
}),
|
|
233
|
+
agentSessionId: 'session-2',
|
|
234
|
+
},
|
|
235
|
+
expected: 'discard',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'waits for an authoritative legacy snapshot',
|
|
239
|
+
input: {
|
|
240
|
+
marker: createRecoveryMarker({
|
|
241
|
+
agentSessionId: 'session-1',
|
|
242
|
+
}),
|
|
243
|
+
agentSessionId: 'session-1',
|
|
244
|
+
legacyStateKnown: false,
|
|
245
|
+
},
|
|
246
|
+
expected: 'wait',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'restores an owned legacy wellness state',
|
|
250
|
+
input: {
|
|
251
|
+
marker: createRecoveryMarker({
|
|
252
|
+
agentSessionId: 'session-1',
|
|
253
|
+
}),
|
|
254
|
+
agentSessionId: 'session-1',
|
|
255
|
+
legacyStateKnown: true,
|
|
256
|
+
legacyAuxCodeId: 'wellness',
|
|
257
|
+
wellnessAuxCodeId: 'wellness',
|
|
258
|
+
},
|
|
259
|
+
expected: 'restore',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'clears a legacy marker when the session already left wellness',
|
|
263
|
+
input: {
|
|
264
|
+
marker: createRecoveryMarker({
|
|
265
|
+
agentSessionId: 'session-1',
|
|
266
|
+
}),
|
|
267
|
+
agentSessionId: 'session-1',
|
|
268
|
+
legacyStateKnown: true,
|
|
269
|
+
legacyAuxCodeId: '0',
|
|
270
|
+
wellnessAuxCodeId: 'wellness',
|
|
271
|
+
},
|
|
272
|
+
expected: 'clear',
|
|
273
|
+
},
|
|
274
|
+
])('$name', ({input, expected}) => {
|
|
275
|
+
expect(getRecoveryDecision(input)).toBe(expected);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('rejects malformed or unsupported recovery markers', () => {
|
|
279
|
+
expect(parseRecoveryMarker('{')).toBeUndefined();
|
|
280
|
+
expect(parseRecoveryMarker(JSON.stringify({version: 2}))).toBeUndefined();
|
|
281
|
+
expect(
|
|
282
|
+
parseRecoveryMarker(
|
|
283
|
+
JSON.stringify({version: 1, agentSessionId: 'session-1', stateModel: 'agent-state-control'})
|
|
284
|
+
)
|
|
285
|
+
).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|