@replanejs/test-suite 0.8.19
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/LICENSE +21 -0
- package/dist/index.cjs +937 -0
- package/dist/index.d.cts +213 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +908 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
const vitest = __toESM(require("vitest"));
|
|
25
|
+
const __replanejs_admin = __toESM(require("@replanejs/admin"));
|
|
26
|
+
const __replanejs_sdk = __toESM(require("@replanejs/sdk"));
|
|
27
|
+
|
|
28
|
+
//#region src/utils.ts
|
|
29
|
+
/**
|
|
30
|
+
* Creates a deferred promise that can be resolved/rejected externally
|
|
31
|
+
*/
|
|
32
|
+
function createDeferred() {
|
|
33
|
+
let resolve;
|
|
34
|
+
let reject;
|
|
35
|
+
const promise = new Promise((res, rej) => {
|
|
36
|
+
resolve = res;
|
|
37
|
+
reject = rej;
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
promise,
|
|
41
|
+
resolve,
|
|
42
|
+
reject
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Waits for a condition to be met or times out.
|
|
47
|
+
* Returns immediately when condition is satisfied.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* let value: string | null = null;
|
|
52
|
+
* client.subscribe("config", (v) => { value = v; });
|
|
53
|
+
*
|
|
54
|
+
* await waitFor(() => value !== null, { timeout: 2000 });
|
|
55
|
+
* expect(value).toBe("expected");
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
async function waitFor(condition, options = {}) {
|
|
59
|
+
const { timeout = 5e3, timeoutMessage = "waitFor timed out" } = options;
|
|
60
|
+
const startTime = Date.now();
|
|
61
|
+
while (true) {
|
|
62
|
+
const result = await condition();
|
|
63
|
+
if (result) return;
|
|
64
|
+
if (Date.now() - startTime >= timeout) throw new Error(timeoutMessage);
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function createSignal() {
|
|
69
|
+
let deferred = createDeferred();
|
|
70
|
+
let triggered = false;
|
|
71
|
+
let value;
|
|
72
|
+
return {
|
|
73
|
+
wait(options = {}) {
|
|
74
|
+
const { timeout = 5e3, timeoutMessage = "Signal wait timed out" } = options;
|
|
75
|
+
return Promise.race([deferred.promise, new Promise((_, reject) => setTimeout(() => reject(new Error(timeoutMessage)), timeout))]);
|
|
76
|
+
},
|
|
77
|
+
trigger(v) {
|
|
78
|
+
if (!triggered) {
|
|
79
|
+
triggered = true;
|
|
80
|
+
value = v;
|
|
81
|
+
deferred.resolve(v);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
isTriggered() {
|
|
85
|
+
return triggered;
|
|
86
|
+
},
|
|
87
|
+
reset() {
|
|
88
|
+
triggered = false;
|
|
89
|
+
value = void 0;
|
|
90
|
+
deferred = createDeferred();
|
|
91
|
+
},
|
|
92
|
+
getValue() {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function createCollector() {
|
|
98
|
+
const values = [];
|
|
99
|
+
const listeners = [];
|
|
100
|
+
const notify = () => {
|
|
101
|
+
for (const listener of listeners) listener();
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
push(value) {
|
|
105
|
+
values.push(value);
|
|
106
|
+
notify();
|
|
107
|
+
},
|
|
108
|
+
getValues() {
|
|
109
|
+
return [...values];
|
|
110
|
+
},
|
|
111
|
+
count() {
|
|
112
|
+
return values.length;
|
|
113
|
+
},
|
|
114
|
+
async waitForCount(count, options = {}) {
|
|
115
|
+
const { timeout = 5e3, timeoutMessage = `Collector timed out waiting for ${count} values` } = options;
|
|
116
|
+
if (values.length >= count) return [...values];
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const timeoutId = setTimeout(() => {
|
|
119
|
+
const idx = listeners.indexOf(listener);
|
|
120
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
121
|
+
reject(new Error(timeoutMessage));
|
|
122
|
+
}, timeout);
|
|
123
|
+
const listener = () => {
|
|
124
|
+
if (values.length >= count) {
|
|
125
|
+
clearTimeout(timeoutId);
|
|
126
|
+
const idx = listeners.indexOf(listener);
|
|
127
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
128
|
+
resolve([...values]);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
listeners.push(listener);
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
async waitFor(predicate, options = {}) {
|
|
135
|
+
const { timeout = 5e3, timeoutMessage = "Collector timed out waiting for matching value" } = options;
|
|
136
|
+
for (const v of values) if (predicate(v)) return v;
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const timeoutId = setTimeout(() => {
|
|
139
|
+
const idx = listeners.indexOf(listener);
|
|
140
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
141
|
+
reject(new Error(timeoutMessage));
|
|
142
|
+
}, timeout);
|
|
143
|
+
const listener = () => {
|
|
144
|
+
const latest = values[values.length - 1];
|
|
145
|
+
if (latest !== void 0 && predicate(latest)) {
|
|
146
|
+
clearTimeout(timeoutId);
|
|
147
|
+
const idx = listeners.indexOf(listener);
|
|
148
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
149
|
+
resolve(latest);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
listeners.push(listener);
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
clear() {
|
|
156
|
+
values.length = 0;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Delay for a specified time (use sparingly in tests)
|
|
162
|
+
*/
|
|
163
|
+
function delay(ms) {
|
|
164
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Generate a unique test identifier
|
|
168
|
+
*/
|
|
169
|
+
function uniqueId(prefix = "test") {
|
|
170
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Sync the edge replica with the database.
|
|
174
|
+
* Requires TESTING_MODE=true on the server.
|
|
175
|
+
*
|
|
176
|
+
* This is useful in tests to ensure the edge replica has
|
|
177
|
+
* received all config changes before making assertions.
|
|
178
|
+
*
|
|
179
|
+
* @param request - The request object containing the edge API base URL and admin API key
|
|
180
|
+
* @param request.edgeApiBaseUrl - The base URL of the edge API (e.g., "http://localhost:8080")
|
|
181
|
+
* @param request.adminApiKey - The admin API key
|
|
182
|
+
*/
|
|
183
|
+
async function syncReplica(request) {
|
|
184
|
+
const { edgeApiBaseUrl, sdkKey } = request;
|
|
185
|
+
const response = await fetch(`${edgeApiBaseUrl}/api/sdk/v1/testing/sync`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: { Authorization: `Bearer ${sdkKey}` }
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const body = await response.text();
|
|
191
|
+
throw new Error(`Failed to sync replica: ${response.status} ${body}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/test-suite.ts
|
|
197
|
+
/**
|
|
198
|
+
* Helper to create a literal value for conditions.
|
|
199
|
+
*/
|
|
200
|
+
function literal(value) {
|
|
201
|
+
return {
|
|
202
|
+
type: "literal",
|
|
203
|
+
value
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const silentLogger = {
|
|
207
|
+
debug: () => {},
|
|
208
|
+
info: () => {},
|
|
209
|
+
warn: () => {},
|
|
210
|
+
error: () => {}
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* Creates a test context with helper methods
|
|
214
|
+
*/
|
|
215
|
+
function createTestContext(admin, workspaceId, projectId, environmentId, sdkKey, options) {
|
|
216
|
+
const defaultTimeout = options.defaultTimeout ?? 1e4;
|
|
217
|
+
function sync() {
|
|
218
|
+
return syncReplica({
|
|
219
|
+
edgeApiBaseUrl: options.edgeApiBaseUrl,
|
|
220
|
+
sdkKey
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
admin,
|
|
225
|
+
workspaceId,
|
|
226
|
+
projectId,
|
|
227
|
+
environmentId,
|
|
228
|
+
sdkKey,
|
|
229
|
+
edgeApiBaseUrl: options.edgeApiBaseUrl,
|
|
230
|
+
adminApiBaseUrl: options.adminApiBaseUrl,
|
|
231
|
+
defaultTimeout,
|
|
232
|
+
sync,
|
|
233
|
+
async createClient(clientOptions) {
|
|
234
|
+
await sync();
|
|
235
|
+
return (0, __replanejs_sdk.createReplaneClient)({
|
|
236
|
+
sdkKey,
|
|
237
|
+
baseUrl: options.edgeApiBaseUrl,
|
|
238
|
+
logger: silentLogger,
|
|
239
|
+
initializationTimeoutMs: defaultTimeout,
|
|
240
|
+
context: clientOptions?.context,
|
|
241
|
+
defaults: clientOptions?.defaults,
|
|
242
|
+
required: clientOptions?.required
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
async createConfig(name, value, configOptions) {
|
|
246
|
+
await admin.configs.create({
|
|
247
|
+
projectId,
|
|
248
|
+
name,
|
|
249
|
+
description: configOptions?.description ?? "",
|
|
250
|
+
editors: [],
|
|
251
|
+
maintainers: [],
|
|
252
|
+
base: {
|
|
253
|
+
value,
|
|
254
|
+
schema: null,
|
|
255
|
+
overrides: configOptions?.overrides ?? []
|
|
256
|
+
},
|
|
257
|
+
variants: []
|
|
258
|
+
});
|
|
259
|
+
await sync();
|
|
260
|
+
},
|
|
261
|
+
async updateConfig(name, value, configOptions) {
|
|
262
|
+
await admin.configs.update({
|
|
263
|
+
projectId,
|
|
264
|
+
configName: name,
|
|
265
|
+
description: configOptions?.description ?? "",
|
|
266
|
+
editors: [],
|
|
267
|
+
base: {
|
|
268
|
+
value,
|
|
269
|
+
schema: null,
|
|
270
|
+
overrides: configOptions?.overrides ?? []
|
|
271
|
+
},
|
|
272
|
+
variants: []
|
|
273
|
+
});
|
|
274
|
+
await sync();
|
|
275
|
+
},
|
|
276
|
+
async deleteConfig(name) {
|
|
277
|
+
await admin.configs.delete({
|
|
278
|
+
projectId,
|
|
279
|
+
configName: name
|
|
280
|
+
});
|
|
281
|
+
await sync();
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Main test suite function
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* import { testSuite } from "@replanejs/test-suite";
|
|
291
|
+
*
|
|
292
|
+
* testSuite({
|
|
293
|
+
* superadminKey: process.env.SUPERADMIN_KEY!,
|
|
294
|
+
* adminApiBaseUrl: "http://localhost:8080",
|
|
295
|
+
* edgeApiBaseUrl: "http://localhost:8080",
|
|
296
|
+
* });
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
function testSuite(options) {
|
|
300
|
+
const { superadminKey, adminApiBaseUrl, edgeApiBaseUrl } = options;
|
|
301
|
+
const defaultTimeout = options.defaultTimeout ?? 1e4;
|
|
302
|
+
(0, vitest.describe)("Replane E2E Test Suite", () => {
|
|
303
|
+
let admin;
|
|
304
|
+
let workspaceId;
|
|
305
|
+
let projectId;
|
|
306
|
+
let environmentId;
|
|
307
|
+
let sdkKey;
|
|
308
|
+
let ctx;
|
|
309
|
+
const activeClients = [];
|
|
310
|
+
(0, vitest.beforeAll)(async () => {
|
|
311
|
+
admin = new __replanejs_admin.ReplaneAdmin({
|
|
312
|
+
apiKey: superadminKey,
|
|
313
|
+
baseUrl: adminApiBaseUrl
|
|
314
|
+
});
|
|
315
|
+
const workspaceName = uniqueId("e2e-workspace");
|
|
316
|
+
const workspaceRes = await admin.workspaces.create({ name: workspaceName });
|
|
317
|
+
workspaceId = workspaceRes.id;
|
|
318
|
+
const projectName = uniqueId("e2e-project");
|
|
319
|
+
const projectRes = await admin.projects.create({
|
|
320
|
+
workspaceId,
|
|
321
|
+
name: projectName,
|
|
322
|
+
description: "E2E test project"
|
|
323
|
+
});
|
|
324
|
+
projectId = projectRes.id;
|
|
325
|
+
const envRes = await admin.environments.list({ projectId });
|
|
326
|
+
const prodEnv = envRes.environments.find((e) => e.name === "Production") ?? envRes.environments[0];
|
|
327
|
+
if (!prodEnv) throw new Error("No environments found");
|
|
328
|
+
environmentId = prodEnv.id;
|
|
329
|
+
const sdkKeyRes = await admin.sdkKeys.create({
|
|
330
|
+
projectId,
|
|
331
|
+
name: uniqueId("e2e-sdk-key"),
|
|
332
|
+
environmentId
|
|
333
|
+
});
|
|
334
|
+
sdkKey = sdkKeyRes.key;
|
|
335
|
+
await syncReplica({
|
|
336
|
+
edgeApiBaseUrl,
|
|
337
|
+
sdkKey
|
|
338
|
+
});
|
|
339
|
+
ctx = createTestContext(admin, workspaceId, projectId, environmentId, sdkKey, options);
|
|
340
|
+
});
|
|
341
|
+
(0, vitest.afterAll)(async () => {
|
|
342
|
+
for (const client of activeClients) try {
|
|
343
|
+
client.close();
|
|
344
|
+
} catch {}
|
|
345
|
+
activeClients.length = 0;
|
|
346
|
+
if (workspaceId) await admin.workspaces.delete({ workspaceId });
|
|
347
|
+
});
|
|
348
|
+
const trackClient = (client) => {
|
|
349
|
+
activeClients.push(client);
|
|
350
|
+
return client;
|
|
351
|
+
};
|
|
352
|
+
(0, vitest.afterEach)(async () => {
|
|
353
|
+
const configs = await admin.configs.list({ projectId });
|
|
354
|
+
for (const config of configs.configs) await admin.configs.delete({
|
|
355
|
+
projectId,
|
|
356
|
+
configName: config.name
|
|
357
|
+
});
|
|
358
|
+
await syncReplica({
|
|
359
|
+
edgeApiBaseUrl,
|
|
360
|
+
sdkKey
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
(0, vitest.describe)("SDK Connection", () => {
|
|
364
|
+
(0, vitest.it)("should connect and receive initial configs", async () => {
|
|
365
|
+
await ctx.createConfig("test-config", "initial-value");
|
|
366
|
+
const client = trackClient(await ctx.createClient({ required: ["test-config"] }));
|
|
367
|
+
const value = client.get("test-config");
|
|
368
|
+
(0, vitest.expect)(value).toBe("initial-value");
|
|
369
|
+
client.close();
|
|
370
|
+
});
|
|
371
|
+
(0, vitest.it)("should handle empty project (no configs)", async () => {
|
|
372
|
+
const emptyProjectRes = await admin.projects.create({
|
|
373
|
+
workspaceId,
|
|
374
|
+
name: uniqueId("empty-project"),
|
|
375
|
+
description: "Empty project for testing"
|
|
376
|
+
});
|
|
377
|
+
const emptyEnvRes = await admin.environments.list({ projectId: emptyProjectRes.id });
|
|
378
|
+
const emptyEnv = emptyEnvRes.environments[0];
|
|
379
|
+
const emptySdkKeyRes = await admin.sdkKeys.create({
|
|
380
|
+
projectId: emptyProjectRes.id,
|
|
381
|
+
name: uniqueId("empty-sdk-key"),
|
|
382
|
+
environmentId: emptyEnv.id
|
|
383
|
+
});
|
|
384
|
+
await syncReplica({
|
|
385
|
+
edgeApiBaseUrl,
|
|
386
|
+
sdkKey
|
|
387
|
+
});
|
|
388
|
+
const client = trackClient(await (0, __replanejs_sdk.createReplaneClient)({
|
|
389
|
+
sdkKey: emptySdkKeyRes.key,
|
|
390
|
+
baseUrl: edgeApiBaseUrl,
|
|
391
|
+
logger: silentLogger,
|
|
392
|
+
initializationTimeoutMs: defaultTimeout
|
|
393
|
+
}));
|
|
394
|
+
(0, vitest.expect)(() => client.get("nonexistent")).toThrow();
|
|
395
|
+
client.close();
|
|
396
|
+
await admin.projects.delete({ projectId: emptyProjectRes.id });
|
|
397
|
+
});
|
|
398
|
+
(0, vitest.it)("should use default values when config not found", async () => {
|
|
399
|
+
const client = trackClient(await ctx.createClient({ defaults: { "missing-config": "default-value" } }));
|
|
400
|
+
const value = client.get("missing-config");
|
|
401
|
+
(0, vitest.expect)(value).toBe("default-value");
|
|
402
|
+
client.close();
|
|
403
|
+
});
|
|
404
|
+
(0, vitest.it)("should throw when required config is missing", { timeout: 15e3 }, async () => {
|
|
405
|
+
await (0, vitest.expect)(ctx.createClient({ required: ["definitely-missing-config"] })).rejects.toThrow();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
(0, vitest.describe)("Get Config", () => {
|
|
409
|
+
(0, vitest.it)("should get string config", async () => {
|
|
410
|
+
await ctx.createConfig("string-config", "hello");
|
|
411
|
+
const client = trackClient(await ctx.createClient({ required: ["string-config"] }));
|
|
412
|
+
(0, vitest.expect)(client.get("string-config")).toBe("hello");
|
|
413
|
+
client.close();
|
|
414
|
+
});
|
|
415
|
+
(0, vitest.it)("should get number config", async () => {
|
|
416
|
+
await ctx.createConfig("number-config", 42);
|
|
417
|
+
const client = trackClient(await ctx.createClient({ required: ["number-config"] }));
|
|
418
|
+
(0, vitest.expect)(client.get("number-config")).toBe(42);
|
|
419
|
+
client.close();
|
|
420
|
+
});
|
|
421
|
+
(0, vitest.it)("should get boolean config", async () => {
|
|
422
|
+
await ctx.createConfig("boolean-config", true);
|
|
423
|
+
const client = trackClient(await ctx.createClient({ required: ["boolean-config"] }));
|
|
424
|
+
(0, vitest.expect)(client.get("boolean-config")).toBe(true);
|
|
425
|
+
client.close();
|
|
426
|
+
});
|
|
427
|
+
(0, vitest.it)("should get object config", async () => {
|
|
428
|
+
const objValue = { nested: { value: "deep" } };
|
|
429
|
+
await ctx.createConfig("object-config", objValue);
|
|
430
|
+
const client = trackClient(await ctx.createClient({ required: ["object-config"] }));
|
|
431
|
+
(0, vitest.expect)(client.get("object-config")).toEqual(objValue);
|
|
432
|
+
client.close();
|
|
433
|
+
});
|
|
434
|
+
(0, vitest.it)("should get array config", async () => {
|
|
435
|
+
const arrValue = [
|
|
436
|
+
1,
|
|
437
|
+
2,
|
|
438
|
+
3
|
|
439
|
+
];
|
|
440
|
+
await ctx.createConfig("array-config", arrValue);
|
|
441
|
+
const client = trackClient(await ctx.createClient({ required: ["array-config"] }));
|
|
442
|
+
(0, vitest.expect)(client.get("array-config")).toEqual(arrValue);
|
|
443
|
+
client.close();
|
|
444
|
+
});
|
|
445
|
+
(0, vitest.it)("should get null config", async () => {
|
|
446
|
+
await ctx.createConfig("null-config", null);
|
|
447
|
+
const client = trackClient(await ctx.createClient({ required: ["null-config"] }));
|
|
448
|
+
(0, vitest.expect)(client.get("null-config")).toBe(null);
|
|
449
|
+
client.close();
|
|
450
|
+
});
|
|
451
|
+
(0, vitest.it)("should return default value when config not found", async () => {
|
|
452
|
+
const client = trackClient(await ctx.createClient());
|
|
453
|
+
const value = client.get("nonexistent", { default: "fallback" });
|
|
454
|
+
(0, vitest.expect)(value).toBe("fallback");
|
|
455
|
+
client.close();
|
|
456
|
+
});
|
|
457
|
+
(0, vitest.it)("should throw ReplaneError when config not found and no default", async () => {
|
|
458
|
+
const client = trackClient(await ctx.createClient());
|
|
459
|
+
try {
|
|
460
|
+
client.get("nonexistent");
|
|
461
|
+
vitest.expect.fail("Should have thrown");
|
|
462
|
+
} catch (error) {
|
|
463
|
+
(0, vitest.expect)(error).toBeInstanceOf(__replanejs_sdk.ReplaneError);
|
|
464
|
+
(0, vitest.expect)(error.code).toBe(__replanejs_sdk.ReplaneErrorCode.NotFound);
|
|
465
|
+
}
|
|
466
|
+
client.close();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
(0, vitest.describe)("Real-time Updates", () => {
|
|
470
|
+
(0, vitest.it)("should receive config updates via subscription", async () => {
|
|
471
|
+
await ctx.createConfig("live-config", "initial");
|
|
472
|
+
const client = trackClient(await ctx.createClient({ required: ["live-config"] }));
|
|
473
|
+
(0, vitest.expect)(client.get("live-config")).toBe("initial");
|
|
474
|
+
const updateSignal = createSignal();
|
|
475
|
+
client.subscribe("live-config", (config) => {
|
|
476
|
+
if (config.value !== "initial") updateSignal.trigger(config.value);
|
|
477
|
+
});
|
|
478
|
+
await ctx.updateConfig("live-config", "updated");
|
|
479
|
+
const newValue = await updateSignal.wait({ timeout: defaultTimeout });
|
|
480
|
+
(0, vitest.expect)(newValue).toBe("updated");
|
|
481
|
+
(0, vitest.expect)(client.get("live-config")).toBe("updated");
|
|
482
|
+
client.close();
|
|
483
|
+
});
|
|
484
|
+
(0, vitest.it)("should receive multiple updates in order", async () => {
|
|
485
|
+
await ctx.createConfig("multi-update-config", 0);
|
|
486
|
+
const client = trackClient(await ctx.createClient({ required: ["multi-update-config"] }));
|
|
487
|
+
(0, vitest.expect)(client.get("multi-update-config")).toBe(0);
|
|
488
|
+
const collector = createCollector();
|
|
489
|
+
client.subscribe("multi-update-config", (config) => {
|
|
490
|
+
const val = config.value;
|
|
491
|
+
if (val > 0) collector.push(val);
|
|
492
|
+
});
|
|
493
|
+
await ctx.updateConfig("multi-update-config", 1);
|
|
494
|
+
await delay(100);
|
|
495
|
+
await ctx.updateConfig("multi-update-config", 2);
|
|
496
|
+
await delay(100);
|
|
497
|
+
await ctx.updateConfig("multi-update-config", 3);
|
|
498
|
+
const values = await collector.waitForCount(3, { timeout: defaultTimeout });
|
|
499
|
+
(0, vitest.expect)(values).toEqual([
|
|
500
|
+
1,
|
|
501
|
+
2,
|
|
502
|
+
3
|
|
503
|
+
]);
|
|
504
|
+
client.close();
|
|
505
|
+
});
|
|
506
|
+
(0, vitest.it)("should handle rapid updates", async () => {
|
|
507
|
+
await ctx.createConfig("rapid-config", 0);
|
|
508
|
+
const client = trackClient(await ctx.createClient({ required: ["rapid-config"] }));
|
|
509
|
+
(0, vitest.expect)(client.get("rapid-config")).toBe(0);
|
|
510
|
+
const collector = createCollector();
|
|
511
|
+
client.subscribe("rapid-config", (config) => {
|
|
512
|
+
collector.push(config.value);
|
|
513
|
+
});
|
|
514
|
+
const updateCount = 10;
|
|
515
|
+
for (let i = 1; i <= updateCount; i++) await ctx.updateConfig("rapid-config", i);
|
|
516
|
+
await collector.waitFor((v) => v === updateCount, { timeout: defaultTimeout });
|
|
517
|
+
(0, vitest.expect)(client.get("rapid-config")).toBe(updateCount);
|
|
518
|
+
client.close();
|
|
519
|
+
});
|
|
520
|
+
(0, vitest.it)("should call global subscription for any config change", async () => {
|
|
521
|
+
await ctx.createConfig("config-a", "a");
|
|
522
|
+
await ctx.createConfig("config-b", "b");
|
|
523
|
+
const client = trackClient(await ctx.createClient({ required: ["config-a", "config-b"] }));
|
|
524
|
+
(0, vitest.expect)(client.get("config-a")).toBe("a");
|
|
525
|
+
(0, vitest.expect)(client.get("config-b")).toBe("b");
|
|
526
|
+
const collector = createCollector();
|
|
527
|
+
client.subscribe((config) => {
|
|
528
|
+
if (config.value === "a-updated" || config.value === "b-updated") collector.push({
|
|
529
|
+
name: config.name,
|
|
530
|
+
value: config.value
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
await ctx.updateConfig("config-a", "a-updated");
|
|
534
|
+
await ctx.updateConfig("config-b", "b-updated");
|
|
535
|
+
await collector.waitForCount(2, { timeout: defaultTimeout });
|
|
536
|
+
const values = collector.getValues();
|
|
537
|
+
const aUpdate = values.find((v) => v.name === "config-a");
|
|
538
|
+
const bUpdate = values.find((v) => v.name === "config-b");
|
|
539
|
+
(0, vitest.expect)(aUpdate?.value).toBe("a-updated");
|
|
540
|
+
(0, vitest.expect)(bUpdate?.value).toBe("b-updated");
|
|
541
|
+
client.close();
|
|
542
|
+
});
|
|
543
|
+
(0, vitest.it)("should allow unsubscribing", async () => {
|
|
544
|
+
await ctx.createConfig("unsub-config", "initial");
|
|
545
|
+
const client = trackClient(await ctx.createClient({ required: ["unsub-config"] }));
|
|
546
|
+
(0, vitest.expect)(client.get("unsub-config")).toBe("initial");
|
|
547
|
+
const collector = createCollector();
|
|
548
|
+
const unsubscribe = client.subscribe("unsub-config", (config) => {
|
|
549
|
+
if (config.value !== "initial") collector.push(config.value);
|
|
550
|
+
});
|
|
551
|
+
await ctx.updateConfig("unsub-config", "update-1");
|
|
552
|
+
await collector.waitForCount(1, { timeout: defaultTimeout });
|
|
553
|
+
unsubscribe();
|
|
554
|
+
await ctx.updateConfig("unsub-config", "update-2");
|
|
555
|
+
await delay(2e3);
|
|
556
|
+
(0, vitest.expect)(collector.count()).toBe(1);
|
|
557
|
+
client.close();
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
(0, vitest.describe)("Override Evaluation", () => {
|
|
561
|
+
(0, vitest.it)("should evaluate equals condition", async () => {
|
|
562
|
+
await ctx.createConfig("env-config", "default", { overrides: [{
|
|
563
|
+
name: "prod-override",
|
|
564
|
+
conditions: [{
|
|
565
|
+
operator: "equals",
|
|
566
|
+
property: "env",
|
|
567
|
+
value: literal("production")
|
|
568
|
+
}],
|
|
569
|
+
value: "production-value"
|
|
570
|
+
}] });
|
|
571
|
+
const client1 = trackClient(await ctx.createClient({ required: ["env-config"] }));
|
|
572
|
+
(0, vitest.expect)(client1.get("env-config")).toBe("default");
|
|
573
|
+
client1.close();
|
|
574
|
+
const client2 = trackClient(await ctx.createClient({
|
|
575
|
+
context: { env: "production" },
|
|
576
|
+
required: ["env-config"]
|
|
577
|
+
}));
|
|
578
|
+
(0, vitest.expect)(client2.get("env-config")).toBe("production-value");
|
|
579
|
+
client2.close();
|
|
580
|
+
const client3 = trackClient(await ctx.createClient({
|
|
581
|
+
context: { env: "staging" },
|
|
582
|
+
required: ["env-config"]
|
|
583
|
+
}));
|
|
584
|
+
(0, vitest.expect)(client3.get("env-config")).toBe("default");
|
|
585
|
+
client3.close();
|
|
586
|
+
});
|
|
587
|
+
(0, vitest.it)("should evaluate in condition", async () => {
|
|
588
|
+
await ctx.createConfig("region-config", "default", { overrides: [{
|
|
589
|
+
name: "western-override",
|
|
590
|
+
conditions: [{
|
|
591
|
+
operator: "in",
|
|
592
|
+
property: "region",
|
|
593
|
+
value: literal(["us", "eu"])
|
|
594
|
+
}],
|
|
595
|
+
value: "western"
|
|
596
|
+
}] });
|
|
597
|
+
const client1 = trackClient(await ctx.createClient({
|
|
598
|
+
context: { region: "us" },
|
|
599
|
+
required: ["region-config"]
|
|
600
|
+
}));
|
|
601
|
+
(0, vitest.expect)(client1.get("region-config")).toBe("western");
|
|
602
|
+
client1.close();
|
|
603
|
+
const client2 = trackClient(await ctx.createClient({
|
|
604
|
+
context: { region: "eu" },
|
|
605
|
+
required: ["region-config"]
|
|
606
|
+
}));
|
|
607
|
+
(0, vitest.expect)(client2.get("region-config")).toBe("western");
|
|
608
|
+
client2.close();
|
|
609
|
+
const client3 = trackClient(await ctx.createClient({
|
|
610
|
+
context: { region: "asia" },
|
|
611
|
+
required: ["region-config"]
|
|
612
|
+
}));
|
|
613
|
+
(0, vitest.expect)(client3.get("region-config")).toBe("default");
|
|
614
|
+
client3.close();
|
|
615
|
+
});
|
|
616
|
+
(0, vitest.it)("should evaluate not_in condition", async () => {
|
|
617
|
+
await ctx.createConfig("allow-config", "allowed", { overrides: [{
|
|
618
|
+
name: "not-blocked-override",
|
|
619
|
+
conditions: [{
|
|
620
|
+
operator: "not_in",
|
|
621
|
+
property: "country",
|
|
622
|
+
value: literal(["blocked1", "blocked2"])
|
|
623
|
+
}],
|
|
624
|
+
value: "not-blocked"
|
|
625
|
+
}] });
|
|
626
|
+
const client1 = trackClient(await ctx.createClient({
|
|
627
|
+
context: { country: "blocked1" },
|
|
628
|
+
required: ["allow-config"]
|
|
629
|
+
}));
|
|
630
|
+
(0, vitest.expect)(client1.get("allow-config")).toBe("allowed");
|
|
631
|
+
client1.close();
|
|
632
|
+
const client2 = trackClient(await ctx.createClient({
|
|
633
|
+
context: { country: "normal" },
|
|
634
|
+
required: ["allow-config"]
|
|
635
|
+
}));
|
|
636
|
+
(0, vitest.expect)(client2.get("allow-config")).toBe("not-blocked");
|
|
637
|
+
client2.close();
|
|
638
|
+
});
|
|
639
|
+
(0, vitest.it)("should evaluate numeric comparison conditions", async () => {
|
|
640
|
+
await ctx.createConfig("tier-config", "free", { overrides: [{
|
|
641
|
+
name: "premium-override",
|
|
642
|
+
conditions: [{
|
|
643
|
+
operator: "greater_than_or_equal",
|
|
644
|
+
property: "level",
|
|
645
|
+
value: literal(10)
|
|
646
|
+
}],
|
|
647
|
+
value: "premium"
|
|
648
|
+
}] });
|
|
649
|
+
const client1 = trackClient(await ctx.createClient({
|
|
650
|
+
context: { level: 5 },
|
|
651
|
+
required: ["tier-config"]
|
|
652
|
+
}));
|
|
653
|
+
(0, vitest.expect)(client1.get("tier-config")).toBe("free");
|
|
654
|
+
client1.close();
|
|
655
|
+
const client2 = trackClient(await ctx.createClient({
|
|
656
|
+
context: { level: 10 },
|
|
657
|
+
required: ["tier-config"]
|
|
658
|
+
}));
|
|
659
|
+
(0, vitest.expect)(client2.get("tier-config")).toBe("premium");
|
|
660
|
+
client2.close();
|
|
661
|
+
const client3 = trackClient(await ctx.createClient({
|
|
662
|
+
context: { level: 15 },
|
|
663
|
+
required: ["tier-config"]
|
|
664
|
+
}));
|
|
665
|
+
(0, vitest.expect)(client3.get("tier-config")).toBe("premium");
|
|
666
|
+
client3.close();
|
|
667
|
+
});
|
|
668
|
+
(0, vitest.it)("should evaluate and condition", async () => {
|
|
669
|
+
await ctx.createConfig("combo-config", "default", { overrides: [{
|
|
670
|
+
name: "combo-override",
|
|
671
|
+
conditions: [{
|
|
672
|
+
operator: "and",
|
|
673
|
+
conditions: [{
|
|
674
|
+
operator: "equals",
|
|
675
|
+
property: "plan",
|
|
676
|
+
value: literal("enterprise")
|
|
677
|
+
}, {
|
|
678
|
+
operator: "equals",
|
|
679
|
+
property: "verified",
|
|
680
|
+
value: literal(true)
|
|
681
|
+
}]
|
|
682
|
+
}],
|
|
683
|
+
value: "enterprise-verified"
|
|
684
|
+
}] });
|
|
685
|
+
const client1 = trackClient(await ctx.createClient({
|
|
686
|
+
context: {
|
|
687
|
+
plan: "enterprise",
|
|
688
|
+
verified: true
|
|
689
|
+
},
|
|
690
|
+
required: ["combo-config"]
|
|
691
|
+
}));
|
|
692
|
+
(0, vitest.expect)(client1.get("combo-config")).toBe("enterprise-verified");
|
|
693
|
+
client1.close();
|
|
694
|
+
const client2 = trackClient(await ctx.createClient({
|
|
695
|
+
context: {
|
|
696
|
+
plan: "enterprise",
|
|
697
|
+
verified: false
|
|
698
|
+
},
|
|
699
|
+
required: ["combo-config"]
|
|
700
|
+
}));
|
|
701
|
+
(0, vitest.expect)(client2.get("combo-config")).toBe("default");
|
|
702
|
+
client2.close();
|
|
703
|
+
});
|
|
704
|
+
(0, vitest.it)("should evaluate or condition", async () => {
|
|
705
|
+
await ctx.createConfig("either-config", "default", { overrides: [{
|
|
706
|
+
name: "privileged-override",
|
|
707
|
+
conditions: [{
|
|
708
|
+
operator: "or",
|
|
709
|
+
conditions: [{
|
|
710
|
+
operator: "equals",
|
|
711
|
+
property: "role",
|
|
712
|
+
value: literal("admin")
|
|
713
|
+
}, {
|
|
714
|
+
operator: "equals",
|
|
715
|
+
property: "role",
|
|
716
|
+
value: literal("superadmin")
|
|
717
|
+
}]
|
|
718
|
+
}],
|
|
719
|
+
value: "privileged"
|
|
720
|
+
}] });
|
|
721
|
+
const client1 = trackClient(await ctx.createClient({
|
|
722
|
+
context: { role: "admin" },
|
|
723
|
+
required: ["either-config"]
|
|
724
|
+
}));
|
|
725
|
+
(0, vitest.expect)(client1.get("either-config")).toBe("privileged");
|
|
726
|
+
client1.close();
|
|
727
|
+
const client2 = trackClient(await ctx.createClient({
|
|
728
|
+
context: { role: "superadmin" },
|
|
729
|
+
required: ["either-config"]
|
|
730
|
+
}));
|
|
731
|
+
(0, vitest.expect)(client2.get("either-config")).toBe("privileged");
|
|
732
|
+
client2.close();
|
|
733
|
+
const client3 = trackClient(await ctx.createClient({
|
|
734
|
+
context: { role: "user" },
|
|
735
|
+
required: ["either-config"]
|
|
736
|
+
}));
|
|
737
|
+
(0, vitest.expect)(client3.get("either-config")).toBe("default");
|
|
738
|
+
client3.close();
|
|
739
|
+
});
|
|
740
|
+
(0, vitest.it)("should allow per-request context override", async () => {
|
|
741
|
+
await ctx.createConfig("dynamic-config", "default", { overrides: [{
|
|
742
|
+
name: "feature-override",
|
|
743
|
+
conditions: [{
|
|
744
|
+
operator: "equals",
|
|
745
|
+
property: "feature",
|
|
746
|
+
value: literal("enabled")
|
|
747
|
+
}],
|
|
748
|
+
value: "feature-on"
|
|
749
|
+
}] });
|
|
750
|
+
const client = trackClient(await ctx.createClient({ required: ["dynamic-config"] }));
|
|
751
|
+
(0, vitest.expect)(client.get("dynamic-config")).toBe("default");
|
|
752
|
+
(0, vitest.expect)(client.get("dynamic-config", { context: { feature: "enabled" } })).toBe("feature-on");
|
|
753
|
+
(0, vitest.expect)(client.get("dynamic-config")).toBe("default");
|
|
754
|
+
client.close();
|
|
755
|
+
});
|
|
756
|
+
(0, vitest.it)("should apply first matching override", async () => {
|
|
757
|
+
await ctx.createConfig("priority-config", "default", { overrides: [
|
|
758
|
+
{
|
|
759
|
+
name: "gold-override",
|
|
760
|
+
conditions: [{
|
|
761
|
+
operator: "equals",
|
|
762
|
+
property: "tier",
|
|
763
|
+
value: literal("gold")
|
|
764
|
+
}],
|
|
765
|
+
value: "gold-value"
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: "silver-override",
|
|
769
|
+
conditions: [{
|
|
770
|
+
operator: "equals",
|
|
771
|
+
property: "tier",
|
|
772
|
+
value: literal("silver")
|
|
773
|
+
}],
|
|
774
|
+
value: "silver-value"
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: "score-override",
|
|
778
|
+
conditions: [{
|
|
779
|
+
operator: "greater_than",
|
|
780
|
+
property: "score",
|
|
781
|
+
value: literal(0)
|
|
782
|
+
}],
|
|
783
|
+
value: "has-score"
|
|
784
|
+
}
|
|
785
|
+
] });
|
|
786
|
+
const client = trackClient(await ctx.createClient({
|
|
787
|
+
context: {
|
|
788
|
+
tier: "gold",
|
|
789
|
+
score: 100
|
|
790
|
+
},
|
|
791
|
+
required: ["priority-config"]
|
|
792
|
+
}));
|
|
793
|
+
(0, vitest.expect)(client.get("priority-config")).toBe("gold-value");
|
|
794
|
+
client.close();
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
(0, vitest.describe)("Snapshot", () => {
|
|
798
|
+
(0, vitest.it)("should create snapshot with current configs", async () => {
|
|
799
|
+
await ctx.createConfig("snap-config-1", "value-1");
|
|
800
|
+
await ctx.createConfig("snap-config-2", "value-2");
|
|
801
|
+
const client = trackClient(await ctx.createClient({ required: ["snap-config-1", "snap-config-2"] }));
|
|
802
|
+
const snapshot = client.getSnapshot();
|
|
803
|
+
(0, vitest.expect)(snapshot.configs).toMatchInlineSnapshot(`
|
|
804
|
+
[
|
|
805
|
+
{
|
|
806
|
+
"name": "snap-config-1",
|
|
807
|
+
"overrides": [],
|
|
808
|
+
"value": "value-1",
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
"name": "snap-config-2",
|
|
812
|
+
"overrides": [],
|
|
813
|
+
"value": "value-2",
|
|
814
|
+
},
|
|
815
|
+
]
|
|
816
|
+
`);
|
|
817
|
+
(0, vitest.expect)(snapshot.configs.map((c) => c.name).sort()).toEqual(["snap-config-1", "snap-config-2"]);
|
|
818
|
+
client.close();
|
|
819
|
+
});
|
|
820
|
+
(0, vitest.it)("should include context in snapshot", async () => {
|
|
821
|
+
await ctx.createConfig("ctx-config", "value");
|
|
822
|
+
const client = trackClient(await ctx.createClient({
|
|
823
|
+
context: { userId: "123" },
|
|
824
|
+
required: ["ctx-config"]
|
|
825
|
+
}));
|
|
826
|
+
const snapshot = client.getSnapshot();
|
|
827
|
+
(0, vitest.expect)(snapshot.context).toEqual({ userId: "123" });
|
|
828
|
+
client.close();
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
(0, vitest.describe)("Error Handling", () => {
|
|
832
|
+
(0, vitest.it)("should throw on invalid SDK key", async () => {
|
|
833
|
+
await (0, vitest.expect)((0, __replanejs_sdk.createReplaneClient)({
|
|
834
|
+
sdkKey: "invalid-key",
|
|
835
|
+
baseUrl: edgeApiBaseUrl,
|
|
836
|
+
logger: silentLogger,
|
|
837
|
+
initializationTimeoutMs: 2e3
|
|
838
|
+
})).rejects.toThrow();
|
|
839
|
+
});
|
|
840
|
+
(0, vitest.it)("should handle closed client gracefully", async () => {
|
|
841
|
+
await ctx.createConfig("close-test", "value");
|
|
842
|
+
const client = trackClient(await ctx.createClient({ required: ["close-test"] }));
|
|
843
|
+
(0, vitest.expect)(client.get("close-test")).toBe("value");
|
|
844
|
+
client.close();
|
|
845
|
+
const cachedValue = client.get("close-test");
|
|
846
|
+
(0, vitest.expect)(cachedValue).toBe("value");
|
|
847
|
+
});
|
|
848
|
+
(0, vitest.it)("should timeout on unreachable server", async () => {
|
|
849
|
+
await (0, vitest.expect)((0, __replanejs_sdk.createReplaneClient)({
|
|
850
|
+
sdkKey: "rp_test",
|
|
851
|
+
baseUrl: "http://localhost:59999",
|
|
852
|
+
logger: silentLogger,
|
|
853
|
+
initializationTimeoutMs: 1e3
|
|
854
|
+
})).rejects.toThrow();
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
(0, vitest.describe)("Config Lifecycle", () => {
|
|
858
|
+
(0, vitest.it)("should handle config creation after client connects", async () => {
|
|
859
|
+
const client = trackClient(await ctx.createClient({ defaults: { "late-config": "waiting" } }));
|
|
860
|
+
const configSignal = createSignal();
|
|
861
|
+
client.subscribe("late-config", (config) => {
|
|
862
|
+
configSignal.trigger(config.value);
|
|
863
|
+
});
|
|
864
|
+
await ctx.createConfig("late-config", "late-value");
|
|
865
|
+
const value = await configSignal.wait({ timeout: defaultTimeout });
|
|
866
|
+
(0, vitest.expect)(value).toBe("late-value");
|
|
867
|
+
(0, vitest.expect)(client.get("late-config")).toBe("late-value");
|
|
868
|
+
client.close();
|
|
869
|
+
});
|
|
870
|
+
(0, vitest.it)("should ignore config deletion on client", async () => {
|
|
871
|
+
await ctx.createConfig("delete-me", "exists");
|
|
872
|
+
const client = trackClient(await ctx.createClient({ required: ["delete-me"] }));
|
|
873
|
+
(0, vitest.expect)(client.get("delete-me")).toBe("exists");
|
|
874
|
+
await ctx.deleteConfig("delete-me");
|
|
875
|
+
await delay(1e3);
|
|
876
|
+
(0, vitest.expect)(client.get("delete-me")).toBe("exists");
|
|
877
|
+
client.close();
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
(0, vitest.describe)("Concurrent Clients", () => {
|
|
881
|
+
(0, vitest.it)("should handle multiple clients with same SDK key", async () => {
|
|
882
|
+
await ctx.createConfig("shared-config", "initial");
|
|
883
|
+
const client1 = trackClient(await ctx.createClient({ required: ["shared-config"] }));
|
|
884
|
+
const client2 = trackClient(await ctx.createClient({ required: ["shared-config"] }));
|
|
885
|
+
(0, vitest.expect)(client1.get("shared-config")).toBe("initial");
|
|
886
|
+
(0, vitest.expect)(client2.get("shared-config")).toBe("initial");
|
|
887
|
+
const signal1 = createSignal();
|
|
888
|
+
const signal2 = createSignal();
|
|
889
|
+
client1.subscribe("shared-config", (c) => {
|
|
890
|
+
if (c.value === "updated") signal1.trigger(c.value);
|
|
891
|
+
});
|
|
892
|
+
client2.subscribe("shared-config", (c) => {
|
|
893
|
+
if (c.value === "updated") signal2.trigger(c.value);
|
|
894
|
+
});
|
|
895
|
+
await ctx.updateConfig("shared-config", "updated");
|
|
896
|
+
const [v1, v2] = await Promise.all([signal1.wait({ timeout: defaultTimeout }), signal2.wait({ timeout: defaultTimeout })]);
|
|
897
|
+
(0, vitest.expect)(v1).toBe("updated");
|
|
898
|
+
(0, vitest.expect)(v2).toBe("updated");
|
|
899
|
+
client1.close();
|
|
900
|
+
client2.close();
|
|
901
|
+
});
|
|
902
|
+
(0, vitest.it)("should isolate context between clients", async () => {
|
|
903
|
+
await ctx.createConfig("context-config", "default", { overrides: [{
|
|
904
|
+
name: "prod-override",
|
|
905
|
+
conditions: [{
|
|
906
|
+
operator: "equals",
|
|
907
|
+
property: "env",
|
|
908
|
+
value: literal("prod")
|
|
909
|
+
}],
|
|
910
|
+
value: "prod-value"
|
|
911
|
+
}] });
|
|
912
|
+
const client1 = trackClient(await ctx.createClient({
|
|
913
|
+
context: { env: "prod" },
|
|
914
|
+
required: ["context-config"]
|
|
915
|
+
}));
|
|
916
|
+
const client2 = trackClient(await ctx.createClient({
|
|
917
|
+
context: { env: "dev" },
|
|
918
|
+
required: ["context-config"]
|
|
919
|
+
}));
|
|
920
|
+
(0, vitest.expect)(client1.get("context-config")).toBe("prod-value");
|
|
921
|
+
(0, vitest.expect)(client2.get("context-config")).toBe("default");
|
|
922
|
+
client1.close();
|
|
923
|
+
client2.close();
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
//#endregion
|
|
930
|
+
exports.createCollector = createCollector;
|
|
931
|
+
exports.createDeferred = createDeferred;
|
|
932
|
+
exports.createSignal = createSignal;
|
|
933
|
+
exports.delay = delay;
|
|
934
|
+
exports.syncReplica = syncReplica;
|
|
935
|
+
exports.testSuite = testSuite;
|
|
936
|
+
exports.uniqueId = uniqueId;
|
|
937
|
+
exports.waitFor = waitFor;
|