@envive-ai/react-hooks 0.3.64 → 0.3.66
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/dist/application/models/featureGates.cjs +1 -2
- package/dist/application/models/featureGates.d.cts +1 -2
- package/dist/application/models/featureGates.d.ts +1 -2
- package/dist/application/models/featureGates.js +1 -2
- package/dist/atoms/app/index.d.cts +7 -7
- package/dist/atoms/app/index.d.ts +7 -7
- package/dist/atoms/app/variant.d.cts +6 -6
- package/dist/atoms/chat/chatState.d.cts +1 -1
- package/dist/atoms/chat/chatState.d.ts +19 -19
- package/dist/atoms/chat/form.d.cts +2 -2
- package/dist/atoms/chat/form.d.ts +2 -2
- package/dist/atoms/chat/index.d.cts +2 -2
- package/dist/atoms/chat/index.d.ts +3 -3
- package/dist/atoms/chat/lastMessage.d.cts +2 -2
- package/dist/atoms/chat/lastMessage.d.ts +2 -2
- package/dist/atoms/chat/messageQueue.d.cts +7 -7
- package/dist/atoms/chat/messageQueue.d.ts +6 -6
- package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
- package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
- package/dist/atoms/chat/renderedWidgetRefs.d.cts +3 -3
- package/dist/atoms/chat/renderedWidgetRefs.d.ts +2 -2
- package/dist/atoms/chat/replies.d.cts +3 -3
- package/dist/atoms/chat/replies.d.ts +3 -3
- package/dist/atoms/chat/suggestions.d.cts +3 -3
- package/dist/atoms/chat/suggestions.d.ts +2 -2
- package/dist/atoms/envive/enviveConfig.d.cts +13 -13
- package/dist/atoms/envive/enviveConfig.d.ts +14 -14
- package/dist/atoms/globalSearch/globalSearch.d.cts +5 -5
- package/dist/atoms/globalSearch/globalSearch.d.ts +5 -5
- package/dist/atoms/org/customerService.d.cts +6 -6
- package/dist/atoms/org/customerService.d.ts +6 -6
- package/dist/atoms/org/graphqlConfig.d.cts +4 -4
- package/dist/atoms/org/graphqlConfig.d.ts +4 -4
- package/dist/atoms/org/newOrgConfigAtom.d.cts +2 -2
- package/dist/atoms/org/newOrgConfigAtom.d.ts +2 -2
- package/dist/atoms/org/orgAnalyticsConfig.d.cts +4 -4
- package/dist/atoms/org/orgAnalyticsConfig.d.ts +4 -4
- package/dist/atoms/search/chatSearch.d.cts +17 -17
- package/dist/atoms/search/chatSearch.d.ts +17 -17
- package/dist/atoms/search/searchAPI.d.cts +13 -13
- package/dist/atoms/search/searchAPI.d.ts +13 -13
- package/dist/atoms/search/utils.d.ts +1 -1
- package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
- package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
- package/dist/contexts/pageContext/pageContext.cjs +12 -24
- package/dist/contexts/pageContext/pageContext.js +12 -24
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.ts +2 -2
- package/dist/contexts/types.d.cts +1 -1
- package/dist/contexts/types.d.ts +1 -1
- package/dist/contexts/typesV3.cjs +8 -1
- package/dist/contexts/typesV3.d.cts +23 -6
- package/dist/contexts/typesV3.d.ts +23 -6
- package/dist/contexts/typesV3.js +8 -2
- package/dist/hooks/GrabAndScroll/useGrabAndScroll.d.ts +2 -2
- package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.ts +2 -2
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/enviveConfigService/enviveConfigService.cjs +21 -35
- package/dist/services/enviveConfigService/enviveConfigService.d.cts +1 -2
- package/dist/services/enviveConfigService/enviveConfigService.d.ts +1 -2
- package/dist/services/enviveConfigService/enviveConfigService.js +21 -35
- package/dist/services/enviveConfigService/fetchBootstrapConfig.cjs +15 -16
- package/dist/services/enviveConfigService/fetchBootstrapConfig.js +15 -16
- package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +1 -69
- package/dist/services/enviveConfigService/fetchGraphQLConfig.js +2 -69
- package/dist/services/featureFlagService/index.cjs +7 -5
- package/dist/services/featureFlagService/index.d.cts +2 -1
- package/dist/services/featureFlagService/index.d.ts +2 -1
- package/dist/services/featureFlagService/index.js +7 -5
- package/package.json +1 -1
- package/src/application/models/featureGates.ts +5 -8
- package/src/contexts/pageContext/__tests__/pageContext.test.tsx +4 -59
- package/src/contexts/pageContext/pageContext.tsx +20 -41
- package/src/contexts/typesV3.ts +22 -4
- package/src/services/enviveConfigService/__tests__/enviveConfigService.test.ts +72 -158
- package/src/services/enviveConfigService/__tests__/fetchBootstrapConfig.test.ts +111 -13
- package/src/services/enviveConfigService/__tests__/fetchGraphQLConfig.test.ts +161 -490
- package/src/services/enviveConfigService/enviveConfigService.ts +51 -87
- package/src/services/enviveConfigService/fetchBootstrapConfig.ts +74 -66
- package/src/services/enviveConfigService/fetchGraphQLConfig.ts +0 -120
- package/src/services/featureFlagService/__tests__/getFeatureFlagOverrides.test.ts +54 -0
- package/src/services/featureFlagService/index.ts +19 -6
- package/dist/application/models/graphql/queries/getWidgetConfigQuery.cjs +0 -42
- package/dist/application/models/graphql/queries/getWidgetConfigQuery.js +0 -41
- package/src/application/models/graphql/queries/getWidgetConfigQuery.ts +0 -55
|
@@ -3,65 +3,15 @@ import { EnviveConfigService } from '../enviveConfigService';
|
|
|
3
3
|
const mockFetch = vi.fn();
|
|
4
4
|
globalThis.fetch = mockFetch;
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Promise.resolve({
|
|
16
|
-
data: {
|
|
17
|
-
me: {
|
|
18
|
-
org: {
|
|
19
|
-
id: 'org-1',
|
|
20
|
-
shortName: 'test-org',
|
|
21
|
-
displayName: 'Test Org',
|
|
22
|
-
domain: 'test.com',
|
|
23
|
-
createdAt: '2024-01-01T00:00:00.000Z',
|
|
24
|
-
updatedAt: '2024-01-01T00:00:00.000Z',
|
|
25
|
-
},
|
|
26
|
-
getWidgetConfig: { productsConfig: {}, gates: [], experimentAssignments: [] },
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
}),
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// getWidgetConfig response with the use_unified_config gate ON (plus a real
|
|
34
|
-
// version pin) — the trigger for the /v1/session/bootstrap upgrade.
|
|
35
|
-
function makeGateOnConfigFetch() {
|
|
36
|
-
mockFetch.mockResolvedValueOnce({
|
|
37
|
-
ok: true,
|
|
38
|
-
json: () =>
|
|
39
|
-
Promise.resolve({
|
|
40
|
-
data: {
|
|
41
|
-
me: {
|
|
42
|
-
org: {
|
|
43
|
-
id: 'org-1',
|
|
44
|
-
shortName: 'test-org',
|
|
45
|
-
displayName: 'Test Org',
|
|
46
|
-
domain: 'test.com',
|
|
47
|
-
createdAt: '2024-01-01T00:00:00.000Z',
|
|
48
|
-
updatedAt: '2024-01-01T00:00:00.000Z',
|
|
49
|
-
},
|
|
50
|
-
getWidgetConfig: {
|
|
51
|
-
productsConfig: {},
|
|
52
|
-
gates: [{ name: 'use_unified_config', value: true }],
|
|
53
|
-
experimentAssignments: [],
|
|
54
|
-
resolution: { baseVersion: '1.0.0', baseVersionSource: 'merchant_layer' },
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
}),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// /v1/session/bootstrap response (REST snake_case). Distinct org id so tests can
|
|
63
|
-
// assert the config actually came from bootstrap, not the getWidgetConfig call.
|
|
64
|
-
function makeBootstrapFetch() {
|
|
6
|
+
// /v1/session/bootstrap is the single session-config call now (getWidgetConfig
|
|
7
|
+
// and the separate GET /v1/org/config were removed). A valid response carries
|
|
8
|
+
// org identity + a NON-EMPTY gates array (the malformed-payload guard requires
|
|
9
|
+
// it) + widget_config. orgId is parameterised so cache tests can assert the
|
|
10
|
+
// config actually came back; urlResolving is null-able for the config-only path.
|
|
11
|
+
function makeBootstrapFetch(
|
|
12
|
+
orgId = 'org-1',
|
|
13
|
+
urlResolving: unknown = { variant_type: 'pdp', specific_details: {}, ready: true },
|
|
14
|
+
) {
|
|
65
15
|
mockFetch.mockResolvedValueOnce({
|
|
66
16
|
ok: true,
|
|
67
17
|
json: () =>
|
|
@@ -69,10 +19,10 @@ function makeBootstrapFetch() {
|
|
|
69
19
|
org_config: {
|
|
70
20
|
org: {
|
|
71
21
|
org: {
|
|
72
|
-
id:
|
|
73
|
-
short_name: '
|
|
74
|
-
display_name: '
|
|
75
|
-
domain: '
|
|
22
|
+
id: orgId,
|
|
23
|
+
short_name: 'test-org',
|
|
24
|
+
display_name: 'Test Org',
|
|
25
|
+
domain: 'test.com',
|
|
76
26
|
status: 'active',
|
|
77
27
|
created_at: '2024-01-01T00:00:00.000Z',
|
|
78
28
|
updated_at: '2024-01-01T00:00:00.000Z',
|
|
@@ -84,7 +34,7 @@ function makeBootstrapFetch() {
|
|
|
84
34
|
},
|
|
85
35
|
widget_config: {
|
|
86
36
|
products_config: {},
|
|
87
|
-
// Assignments come from widget_config (
|
|
37
|
+
// Assignments come from widget_config (ungated).
|
|
88
38
|
experiment_assignments: [
|
|
89
39
|
{
|
|
90
40
|
layer_name: 'ml',
|
|
@@ -95,7 +45,7 @@ function makeBootstrapFetch() {
|
|
|
95
45
|
],
|
|
96
46
|
resolution: { base_version: '1.0.0', base_version_source: 'merchant_layer' },
|
|
97
47
|
},
|
|
98
|
-
url_resolving:
|
|
48
|
+
url_resolving: urlResolving,
|
|
99
49
|
}),
|
|
100
50
|
});
|
|
101
51
|
}
|
|
@@ -125,17 +75,18 @@ afterEach(() => {
|
|
|
125
75
|
});
|
|
126
76
|
|
|
127
77
|
describe('EnviveConfigService caching', () => {
|
|
128
|
-
it('fetches the
|
|
129
|
-
|
|
78
|
+
it('fetches the session config once on a cold cache', async () => {
|
|
79
|
+
makeBootstrapFetch();
|
|
130
80
|
|
|
131
81
|
const result = await makeService().getEnviveConfig();
|
|
132
82
|
|
|
133
83
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(mockFetch.mock.calls[0][0]).toContain('/v1/session/bootstrap');
|
|
134
85
|
expect(result.org.org.id).toBe('org-1');
|
|
135
86
|
});
|
|
136
87
|
|
|
137
88
|
it('serves a warm cache to a new instance with identical params without hitting the network', async () => {
|
|
138
|
-
|
|
89
|
+
makeBootstrapFetch();
|
|
139
90
|
await makeService().getEnviveConfig();
|
|
140
91
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
141
92
|
|
|
@@ -151,13 +102,13 @@ describe('EnviveConfigService caching', () => {
|
|
|
151
102
|
vi.useFakeTimers();
|
|
152
103
|
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
153
104
|
|
|
154
|
-
|
|
105
|
+
makeBootstrapFetch();
|
|
155
106
|
await makeService().getEnviveConfig();
|
|
156
107
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
157
108
|
|
|
158
109
|
vi.setSystemTime(new Date('2026-01-01T00:05:01.000Z'));
|
|
159
110
|
|
|
160
|
-
|
|
111
|
+
makeBootstrapFetch();
|
|
161
112
|
await makeService().getEnviveConfig();
|
|
162
113
|
|
|
163
114
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
@@ -168,11 +119,11 @@ describe('EnviveConfigService caching', () => {
|
|
|
168
119
|
async param => {
|
|
169
120
|
window.history.replaceState({}, '', `/?${param}=1.2.3`);
|
|
170
121
|
|
|
171
|
-
|
|
122
|
+
makeBootstrapFetch();
|
|
172
123
|
await makeService().getEnviveConfig();
|
|
173
124
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
174
125
|
|
|
175
|
-
|
|
126
|
+
makeBootstrapFetch();
|
|
176
127
|
await makeService().getEnviveConfig();
|
|
177
128
|
|
|
178
129
|
// No cache read/write happened, so a second instance refetches instead
|
|
@@ -183,33 +134,33 @@ describe('EnviveConfigService caching', () => {
|
|
|
183
134
|
|
|
184
135
|
it('does not read a version-pinned cache entry after the override is removed', async () => {
|
|
185
136
|
window.history.replaceState({}, '', '/?envive_config_version=1.2.3');
|
|
186
|
-
|
|
137
|
+
makeBootstrapFetch();
|
|
187
138
|
await makeService().getEnviveConfig();
|
|
188
139
|
|
|
189
140
|
window.history.replaceState({}, '', '/');
|
|
190
|
-
|
|
141
|
+
makeBootstrapFetch();
|
|
191
142
|
await makeService().getEnviveConfig();
|
|
192
143
|
|
|
193
144
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
194
145
|
});
|
|
195
146
|
|
|
196
147
|
it('keeps separate cache entries per userId', async () => {
|
|
197
|
-
|
|
148
|
+
makeBootstrapFetch();
|
|
198
149
|
await makeService({ userId: 'user-1' }).getEnviveConfig();
|
|
199
150
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
200
151
|
|
|
201
|
-
|
|
152
|
+
makeBootstrapFetch();
|
|
202
153
|
await makeService({ userId: 'user-2' }).getEnviveConfig();
|
|
203
154
|
|
|
204
155
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
205
156
|
});
|
|
206
157
|
|
|
207
158
|
it('keeps separate cache entries per namespace/source/baseUrl/apiKey', async () => {
|
|
208
|
-
|
|
159
|
+
makeBootstrapFetch();
|
|
209
160
|
await makeService({ namespace: 'ns-a' }).getEnviveConfig();
|
|
210
161
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
211
162
|
|
|
212
|
-
|
|
163
|
+
makeBootstrapFetch();
|
|
213
164
|
await makeService({ namespace: 'ns-b' }).getEnviveConfig();
|
|
214
165
|
|
|
215
166
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
@@ -220,7 +171,7 @@ describe('EnviveConfigService caching', () => {
|
|
|
220
171
|
throw new Error('quota exceeded');
|
|
221
172
|
});
|
|
222
173
|
|
|
223
|
-
|
|
174
|
+
makeBootstrapFetch();
|
|
224
175
|
|
|
225
176
|
const result = await makeService().getEnviveConfig();
|
|
226
177
|
|
|
@@ -232,7 +183,7 @@ describe('EnviveConfigService caching', () => {
|
|
|
232
183
|
throw new Error('storage disabled');
|
|
233
184
|
});
|
|
234
185
|
|
|
235
|
-
|
|
186
|
+
makeBootstrapFetch();
|
|
236
187
|
|
|
237
188
|
const result = await makeService().getEnviveConfig();
|
|
238
189
|
|
|
@@ -241,47 +192,34 @@ describe('EnviveConfigService caching', () => {
|
|
|
241
192
|
|
|
242
193
|
it('fails open and refetches when a cached entry is corrupted JSON', async () => {
|
|
243
194
|
const service = makeService();
|
|
244
|
-
|
|
195
|
+
makeBootstrapFetch();
|
|
245
196
|
await service.getEnviveConfig();
|
|
246
197
|
const [key] = Object.keys(sessionStorage);
|
|
247
198
|
sessionStorage.setItem(key, 'not json');
|
|
248
199
|
|
|
249
|
-
|
|
200
|
+
makeBootstrapFetch();
|
|
250
201
|
const result = await makeService().getEnviveConfig();
|
|
251
202
|
|
|
252
203
|
expect(result.org.org.id).toBe('org-1');
|
|
253
204
|
});
|
|
254
205
|
});
|
|
255
206
|
|
|
256
|
-
describe('EnviveConfigService bootstrap
|
|
257
|
-
// The injection bundle supplies env + contextSource
|
|
258
|
-
// is
|
|
207
|
+
describe('EnviveConfigService bootstrap request', () => {
|
|
208
|
+
// The injection bundle supplies env + contextSource, which request bootstrap's
|
|
209
|
+
// url_resolving branch. Without them the branch is omitted (see the config-only
|
|
210
|
+
// case below), but bootstrap is still the config call either way.
|
|
259
211
|
const bootstrapService = () => makeService({ env: 'dev', contextSource: 'app' });
|
|
260
212
|
|
|
261
|
-
it('
|
|
262
|
-
|
|
213
|
+
it('issues a single /v1/session/bootstrap call and returns its config', async () => {
|
|
214
|
+
makeBootstrapFetch('org-boot');
|
|
263
215
|
|
|
264
216
|
const service = bootstrapService();
|
|
265
217
|
const result = await service.getEnviveConfig();
|
|
266
218
|
|
|
267
219
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
268
|
-
expect(mockFetch.mock.calls[0][0]).toContain('/v1/
|
|
269
|
-
expect(result.org.org.id).toBe('org-1');
|
|
270
|
-
expect(service.takeBootstrapUrlResolving()).toBeNull();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('calls /v1/session/bootstrap and uses ITS config when the gate is on', async () => {
|
|
274
|
-
makeGateOnConfigFetch();
|
|
275
|
-
makeBootstrapFetch();
|
|
276
|
-
|
|
277
|
-
const service = bootstrapService();
|
|
278
|
-
const result = await service.getEnviveConfig();
|
|
279
|
-
|
|
280
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
281
|
-
expect(mockFetch.mock.calls[1][0]).toContain('/v1/session/bootstrap');
|
|
282
|
-
// config is bootstrap's, not the getWidgetConfig call used to read the gate.
|
|
220
|
+
expect(mockFetch.mock.calls[0][0]).toContain('/v1/session/bootstrap');
|
|
283
221
|
expect(result.org.org.id).toBe('org-boot');
|
|
284
|
-
expect(result.org.org.short_name).toBe('
|
|
222
|
+
expect(result.org.org.short_name).toBe('test-org');
|
|
285
223
|
// gates: snake group_name mapped to camel groupName.
|
|
286
224
|
expect(result.gates).toContainEqual({
|
|
287
225
|
name: 'use_unified_config',
|
|
@@ -300,7 +238,6 @@ describe('EnviveConfigService bootstrap upgrade (use_unified_config)', () => {
|
|
|
300
238
|
});
|
|
301
239
|
|
|
302
240
|
it('stashes bootstrap url_resolving (paired with its URL) for the warm-up to consume once', async () => {
|
|
303
|
-
makeGateOnConfigFetch();
|
|
304
241
|
makeBootstrapFetch();
|
|
305
242
|
|
|
306
243
|
const service = bootstrapService();
|
|
@@ -315,96 +252,73 @@ describe('EnviveConfigService bootstrap upgrade (use_unified_config)', () => {
|
|
|
315
252
|
expect(service.takeBootstrapUrlResolving()).toBeNull();
|
|
316
253
|
});
|
|
317
254
|
|
|
318
|
-
it('sends url_resolving context
|
|
319
|
-
makeGateOnConfigFetch();
|
|
255
|
+
it('sends the url_resolving context (no org identity), gate names, and namespace in the body', async () => {
|
|
320
256
|
makeBootstrapFetch();
|
|
321
257
|
|
|
322
258
|
await bootstrapService().getEnviveConfig();
|
|
323
259
|
|
|
324
|
-
const [, options] = mockFetch.mock.calls[
|
|
260
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
325
261
|
const body = JSON.parse(options.body as string);
|
|
326
262
|
expect(body.include_feature_gates).toContain('use_unified_config');
|
|
327
263
|
// Scopes org_config.configs so bootstrap doesn't dump internal configs to the browser.
|
|
328
264
|
expect(body.namespace).toBe('spiffy-react-components');
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
});
|
|
335
|
-
// The version comes ONLY from the URL query param (none set here) — never from
|
|
336
|
-
// getWidgetConfig's resolution, so bootstrap doesn't depend on the discarded
|
|
337
|
-
// getWidgetConfig response.
|
|
265
|
+
// org identity is NOT sent — the server fills it from the authenticated org.
|
|
266
|
+
expect(body.url_resolving.context).toMatchObject({ source: 'app', env: 'dev' });
|
|
267
|
+
expect(body.url_resolving.context.org_id).toBeUndefined();
|
|
268
|
+
expect(body.url_resolving.context.org_short_name).toBeUndefined();
|
|
269
|
+
// No config-version override set here.
|
|
338
270
|
expect(body.url_resolving.org_base_config_version).toBeUndefined();
|
|
339
271
|
expect(body.url_resolving.override_config_version).toBeUndefined();
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
expect(body.url_resolving.feature_gates.use_unified_config).toBe(true);
|
|
272
|
+
// No client gate overrides set → empty map (server resolves the real gates).
|
|
273
|
+
expect(body.url_resolving.feature_gates).toEqual({});
|
|
343
274
|
// the persistent chat session id is threaded (not an empty string).
|
|
344
275
|
expect(body.url_resolving.context.chat_id).toBeTruthy();
|
|
345
276
|
});
|
|
346
277
|
|
|
347
|
-
it('
|
|
348
|
-
//
|
|
349
|
-
//
|
|
278
|
+
it('forwards ?<gate>=true overrides in the bootstrap feature_gates it sends', async () => {
|
|
279
|
+
// Client overrides (query param) must reach bootstrap's url_resolving so they
|
|
280
|
+
// still drive page-variant selection; the server merges them over its gates.
|
|
350
281
|
window.history.replaceState({}, '', '/?use_unified_config=true&is_empty_div_plp_enabled=true');
|
|
351
|
-
makeOkConfigFetch(); // gates: [] — the ?use_unified_config=true override drives the bootstrap path
|
|
352
282
|
makeBootstrapFetch();
|
|
353
283
|
|
|
354
284
|
await bootstrapService().getEnviveConfig();
|
|
355
285
|
|
|
356
|
-
const body = JSON.parse(mockFetch.mock.calls[
|
|
286
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
|
|
287
|
+
expect(body.url_resolving.feature_gates.use_unified_config).toBe(true);
|
|
357
288
|
expect(body.url_resolving.feature_gates.is_empty_div_plp_enabled).toBe(true);
|
|
358
289
|
});
|
|
359
290
|
|
|
360
|
-
it('
|
|
361
|
-
|
|
291
|
+
it('omits the url_resolving branch for non-bundle callers (no env/contextSource) but still fetches config', async () => {
|
|
292
|
+
makeBootstrapFetch('org-1', null);
|
|
362
293
|
|
|
363
|
-
const
|
|
294
|
+
const service = makeService(); // no env/contextSource
|
|
295
|
+
const result = await service.getEnviveConfig();
|
|
364
296
|
|
|
365
297
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
298
|
+
expect(mockFetch.mock.calls[0][0]).toContain('/v1/session/bootstrap');
|
|
299
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
|
|
300
|
+
expect(body.url_resolving).toBeUndefined();
|
|
366
301
|
expect(result.org.org.id).toBe('org-1');
|
|
302
|
+
expect(service.takeBootstrapUrlResolving()).toBeNull();
|
|
367
303
|
});
|
|
368
304
|
|
|
369
|
-
it('
|
|
370
|
-
makeGateOnConfigFetch();
|
|
305
|
+
it('throws (aborting injection) when bootstrap fails — there is no getWidgetConfig fallback', async () => {
|
|
371
306
|
mockFetch.mockResolvedValueOnce({ ok: false, statusText: 'Bad Gateway' });
|
|
372
307
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
377
|
-
// degraded to the proven path rather than aborting.
|
|
378
|
-
expect(result.org.org.id).toBe('org-1');
|
|
379
|
-
expect(service.takeBootstrapUrlResolving()).toBeNull();
|
|
308
|
+
await expect(bootstrapService().getEnviveConfig()).rejects.toThrow(
|
|
309
|
+
'Bootstrap config request failed',
|
|
310
|
+
);
|
|
380
311
|
});
|
|
381
312
|
|
|
382
|
-
it('
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
//
|
|
313
|
+
it('throws when bootstrap returns a malformed 2xx payload', async () => {
|
|
314
|
+
// 2xx but missing required sections — must NOT become a config with empty
|
|
315
|
+
// gates / mock products (which would disable the client session). No fallback
|
|
316
|
+
// left, so the throw propagates and injection aborts.
|
|
386
317
|
mockFetch.mockResolvedValueOnce({
|
|
387
318
|
ok: true,
|
|
388
319
|
json: () => Promise.resolve({ org_config: {}, widget_config: null }),
|
|
389
320
|
});
|
|
390
321
|
|
|
391
|
-
|
|
392
|
-
const result = await service.getEnviveConfig();
|
|
393
|
-
|
|
394
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
395
|
-
expect(result.org.org.id).toBe('org-1'); // getWidgetConfig's, not bootstrap's empty payload
|
|
396
|
-
expect(service.takeBootstrapUrlResolving()).toBeNull();
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('honors a ?use_unified_config=true override when the gate itself is off', async () => {
|
|
400
|
-
window.history.replaceState({}, '', '/?use_unified_config=true');
|
|
401
|
-
makeOkConfigFetch(); // gates: [] — the override, not the gate, forces bootstrap
|
|
402
|
-
makeBootstrapFetch();
|
|
403
|
-
|
|
404
|
-
const result = await bootstrapService().getEnviveConfig();
|
|
405
|
-
|
|
406
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
407
|
-
expect(mockFetch.mock.calls[1][0]).toContain('/v1/session/bootstrap');
|
|
408
|
-
expect(result.org.org.id).toBe('org-boot');
|
|
322
|
+
await expect(bootstrapService().getEnviveConfig()).rejects.toThrow('missing required sections');
|
|
409
323
|
});
|
|
410
324
|
});
|
|
@@ -67,9 +67,9 @@ const baseParams: FetchBootstrapConfigParams = {
|
|
|
67
67
|
source: 'app',
|
|
68
68
|
namespace: 'spiffy-react-components',
|
|
69
69
|
gateNames: ['use_unified_config', 'is_client_session_enabled'],
|
|
70
|
+
// url_resolving inputs (org identity is NOT sent — the server fills it from the
|
|
71
|
+
// authenticated org). Absent url ⇒ bootstrap omits the url_resolving branch.
|
|
70
72
|
url: 'https://shop.example.com/products/thing',
|
|
71
|
-
orgId: 'org-1',
|
|
72
|
-
orgShortName: 'test-org',
|
|
73
73
|
contextSource: 'app',
|
|
74
74
|
env: 'prod',
|
|
75
75
|
featureGates: { use_unified_config: true, is_empty_div_plp_enabled: true },
|
|
@@ -180,20 +180,38 @@ describe('fetchBootstrapConfig', () => {
|
|
|
180
180
|
});
|
|
181
181
|
expect(body.url_resolving).toMatchObject({
|
|
182
182
|
url: 'https://shop.example.com/products/thing',
|
|
183
|
-
// Override-
|
|
183
|
+
// Override-only gates forwarded so client overrides reach page-variant resolution.
|
|
184
184
|
feature_gates: { use_unified_config: true, is_empty_div_plp_enabled: true },
|
|
185
185
|
});
|
|
186
|
-
// No org_base_config_version — the version comes only from the URL query param
|
|
187
|
-
// (override_config_version), never from getWidgetConfig's resolution.
|
|
186
|
+
// No org_base_config_version — the version comes only from the URL query param.
|
|
188
187
|
expect(body.url_resolving.org_base_config_version).toBeUndefined();
|
|
188
|
+
// org identity is NOT sent — the server fills org_id/org_short_name from the
|
|
189
|
+
// authenticated org (bootstrap-only has no getWidgetConfig to source them from).
|
|
189
190
|
expect(body.url_resolving.context).toEqual({
|
|
190
191
|
user_id: 'user-1',
|
|
191
|
-
org_id: 'org-1',
|
|
192
|
-
org_short_name: 'test-org',
|
|
193
192
|
chat_id: 'chat-9',
|
|
194
193
|
source: 'app',
|
|
195
194
|
env: 'prod',
|
|
196
195
|
});
|
|
196
|
+
expect(body.url_resolving.context.org_id).toBeUndefined();
|
|
197
|
+
expect(body.url_resolving.context.org_short_name).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('omits the url_resolving branch from the request when no url is supplied', async () => {
|
|
201
|
+
makeBootstrapResponse();
|
|
202
|
+
|
|
203
|
+
// A disabled session (envive_on=false) requests config only — no page to resolve.
|
|
204
|
+
const { url, contextSource, env, featureGates, ...configOnly } = baseParams;
|
|
205
|
+
await fetchBootstrapConfig(configOnly);
|
|
206
|
+
|
|
207
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
|
|
208
|
+
// Config inputs still sent; the whole url_resolving object is absent.
|
|
209
|
+
expect(body).toMatchObject({
|
|
210
|
+
user_id: 'user-1',
|
|
211
|
+
namespace: 'spiffy-react-components',
|
|
212
|
+
include_feature_gates: ['use_unified_config', 'is_client_session_enabled'],
|
|
213
|
+
});
|
|
214
|
+
expect(body.url_resolving).toBeUndefined();
|
|
197
215
|
});
|
|
198
216
|
|
|
199
217
|
it('threads a config-version query param into version + override_config_version', async () => {
|
|
@@ -207,7 +225,67 @@ describe('fetchBootstrapConfig', () => {
|
|
|
207
225
|
expect(body.url_resolving.override_config_version).toBe('9.9.9');
|
|
208
226
|
});
|
|
209
227
|
|
|
210
|
-
it('
|
|
228
|
+
it('degrades a malformed products_config to mock WITHOUT losing org/gates/experiments', async () => {
|
|
229
|
+
// A 2xx with valid org/gates/widget_config but a products_config that throws
|
|
230
|
+
// in transformV3ProductsConfig (invalid floating-button position). With no
|
|
231
|
+
// getWidgetConfig fallback left, this catch is the SOLE thing keeping a bad
|
|
232
|
+
// products config from aborting injection — it must null colors/frontend only
|
|
233
|
+
// and preserve org identity + gates + experiment_assignments + resolution.
|
|
234
|
+
mockFetch.mockResolvedValueOnce({
|
|
235
|
+
ok: true,
|
|
236
|
+
json: () =>
|
|
237
|
+
Promise.resolve({
|
|
238
|
+
org_config: {
|
|
239
|
+
org: { org: { id: 'org-boot', short_name: 'boot-org' } },
|
|
240
|
+
gates: [{ name: 'use_unified_config', value: true, group_name: 'Treatment' }],
|
|
241
|
+
},
|
|
242
|
+
widget_config: {
|
|
243
|
+
products_config: {
|
|
244
|
+
v_three_config: {
|
|
245
|
+
values: {
|
|
246
|
+
colors: { values: { text_primary: '#000000' } },
|
|
247
|
+
ui_configs: {
|
|
248
|
+
floating_button: { position: 'top-center' }, // invalid → transform throws
|
|
249
|
+
look_and_feel: { typography: {} },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
experiment_assignments: [
|
|
255
|
+
{
|
|
256
|
+
layer_name: 'ml',
|
|
257
|
+
namespace: 'ml',
|
|
258
|
+
allocated_experiment_name: 'exp_a',
|
|
259
|
+
group_name: 'A',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
resolution: { base_version: '2.0.0', base_version_source: 'merchant_layer' },
|
|
263
|
+
},
|
|
264
|
+
url_resolving: null,
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const { config } = await fetchBootstrapConfig(baseParams);
|
|
269
|
+
|
|
270
|
+
// org / gates / experiments survived the products-config failure.
|
|
271
|
+
expect(config.org.org.id).toBe('org-boot');
|
|
272
|
+
expect(config.gates).toContainEqual({
|
|
273
|
+
name: 'use_unified_config',
|
|
274
|
+
value: true,
|
|
275
|
+
groupName: 'Treatment',
|
|
276
|
+
});
|
|
277
|
+
expect(config.experiment_assignments).toEqual([
|
|
278
|
+
{ layer_name: 'ml', namespace: 'ml', allocated_experiment_name: 'exp_a', group_name: 'A' },
|
|
279
|
+
]);
|
|
280
|
+
// colors/frontend degraded to undefined, resolution still attached.
|
|
281
|
+
expect(config.orgConfig).toEqual({
|
|
282
|
+
colorsConfig: undefined,
|
|
283
|
+
frontendConfig: undefined,
|
|
284
|
+
resolution: { baseVersion: '2.0.0', baseVersionSource: 'merchant_layer' },
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('throws on a non-ok response so the caller aborts injection (no fallback)', async () => {
|
|
211
289
|
mockFetch.mockResolvedValueOnce({ ok: false, statusText: 'Bad Gateway' });
|
|
212
290
|
|
|
213
291
|
await expect(fetchBootstrapConfig(baseParams)).rejects.toThrow(
|
|
@@ -225,14 +303,13 @@ describe('fetchBootstrapConfig', () => {
|
|
|
225
303
|
'partial org_config (no org.org)',
|
|
226
304
|
{ org_config: { org: {}, gates: okGates }, widget_config: okWidget },
|
|
227
305
|
],
|
|
228
|
-
['absent gates', { org_config: { org: { org: { id: 'o' } } }, widget_config: okWidget }],
|
|
229
306
|
[
|
|
230
|
-
'
|
|
231
|
-
{ org_config: { org: { org: { id: 'o' } }
|
|
307
|
+
'absent gates (not an array)',
|
|
308
|
+
{ org_config: { org: { org: { id: 'o' } } }, widget_config: okWidget },
|
|
232
309
|
],
|
|
233
310
|
['missing widget_config', { org_config: okOrg, widget_config: null }],
|
|
234
311
|
])(
|
|
235
|
-
'throws on a 2xx with %s so the caller
|
|
312
|
+
'throws on a 2xx with %s so the caller aborts injection instead of using empty config',
|
|
236
313
|
async (_label, payload) => {
|
|
237
314
|
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(payload) });
|
|
238
315
|
|
|
@@ -240,6 +317,27 @@ describe('fetchBootstrapConfig', () => {
|
|
|
240
317
|
},
|
|
241
318
|
);
|
|
242
319
|
|
|
320
|
+
it('accepts an EMPTY (but present) gates array as a valid all-disabled config', async () => {
|
|
321
|
+
// An empty gates array is a legitimate "everything off" response (no user to
|
|
322
|
+
// bucket, or every requested gate off). It must NOT be rejected as malformed:
|
|
323
|
+
// FeatureFlagService resolves every gate to false → the session disables
|
|
324
|
+
// gracefully (with the init event still firing), rather than aborting injection.
|
|
325
|
+
mockFetch.mockResolvedValueOnce({
|
|
326
|
+
ok: true,
|
|
327
|
+
json: () =>
|
|
328
|
+
Promise.resolve({
|
|
329
|
+
org_config: { org: { org: { id: 'org-boot', short_name: 'boot-org' } }, gates: [] },
|
|
330
|
+
widget_config: { products_config: {}, experiment_assignments: [] },
|
|
331
|
+
url_resolving: null,
|
|
332
|
+
}),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const { config } = await fetchBootstrapConfig(baseParams);
|
|
336
|
+
|
|
337
|
+
expect(config.org.org.id).toBe('org-boot');
|
|
338
|
+
expect(config.gates).toEqual([]);
|
|
339
|
+
});
|
|
340
|
+
|
|
243
341
|
it('passes an AbortSignal to fetch (timeout wiring)', async () => {
|
|
244
342
|
makeBootstrapResponse();
|
|
245
343
|
|
|
@@ -249,7 +347,7 @@ describe('fetchBootstrapConfig', () => {
|
|
|
249
347
|
expect(options.signal).toBeInstanceOf(AbortSignal);
|
|
250
348
|
});
|
|
251
349
|
|
|
252
|
-
it('aborts and rejects when the request exceeds the timeout
|
|
350
|
+
it('aborts and rejects when the request exceeds the timeout (injection aborts, no fallback)', async () => {
|
|
253
351
|
vi.useFakeTimers();
|
|
254
352
|
// Mimic real fetch: reject with an AbortError once its signal aborts.
|
|
255
353
|
mockFetch.mockImplementationOnce(
|