flagsmith-nodejs 6.2.0 → 7.0.3

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.
Files changed (100) hide show
  1. package/.github/workflows/publish.yml +8 -5
  2. package/.github/workflows/pull_request.yaml +3 -0
  3. package/.gitmodules +1 -1
  4. package/.husky/pre-commit +1 -0
  5. package/.nvmrc +1 -0
  6. package/.release-please-manifest.json +1 -1
  7. package/CHANGELOG.md +49 -0
  8. package/build/cjs/flagsmith-engine/environments/models.d.ts +2 -1
  9. package/build/cjs/flagsmith-engine/environments/models.js +3 -1
  10. package/build/cjs/flagsmith-engine/environments/util.js +1 -1
  11. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  12. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +8 -0
  13. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  14. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.js +156 -0
  15. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  16. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.js +8 -0
  17. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  18. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +8 -0
  19. package/build/cjs/flagsmith-engine/evaluation/models.d.ts +50 -0
  20. package/build/cjs/flagsmith-engine/evaluation/models.js +26 -0
  21. package/build/cjs/flagsmith-engine/features/models.js +1 -1
  22. package/build/cjs/flagsmith-engine/features/types.d.ts +5 -0
  23. package/build/cjs/flagsmith-engine/features/types.js +9 -0
  24. package/build/cjs/flagsmith-engine/features/util.d.ts +1 -0
  25. package/build/cjs/flagsmith-engine/features/util.js +5 -1
  26. package/build/cjs/flagsmith-engine/index.d.ts +61 -9
  27. package/build/cjs/flagsmith-engine/index.js +176 -56
  28. package/build/cjs/flagsmith-engine/segments/constants.d.ts +1 -0
  29. package/build/cjs/flagsmith-engine/segments/constants.js +2 -1
  30. package/build/cjs/flagsmith-engine/segments/evaluators.d.ts +41 -7
  31. package/build/cjs/flagsmith-engine/segments/evaluators.js +136 -24
  32. package/build/cjs/flagsmith-engine/segments/models.d.ts +9 -4
  33. package/build/cjs/flagsmith-engine/segments/models.js +115 -13
  34. package/build/cjs/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  35. package/build/cjs/flagsmith-engine/utils/hashing/index.js +4 -4
  36. package/build/cjs/sdk/index.d.ts +1 -3
  37. package/build/cjs/sdk/index.js +22 -19
  38. package/build/cjs/sdk/models.d.ts +8 -1
  39. package/build/cjs/sdk/models.js +29 -1
  40. package/build/esm/flagsmith-engine/environments/models.d.ts +2 -1
  41. package/build/esm/flagsmith-engine/environments/models.js +3 -1
  42. package/build/esm/flagsmith-engine/environments/util.js +1 -1
  43. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  44. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +7 -0
  45. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  46. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.js +152 -0
  47. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  48. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.js +7 -0
  49. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  50. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +7 -0
  51. package/build/esm/flagsmith-engine/evaluation/models.d.ts +50 -0
  52. package/build/esm/flagsmith-engine/evaluation/models.js +9 -0
  53. package/build/esm/flagsmith-engine/features/models.js +2 -2
  54. package/build/esm/flagsmith-engine/features/types.d.ts +5 -0
  55. package/build/esm/flagsmith-engine/features/types.js +6 -0
  56. package/build/esm/flagsmith-engine/features/util.d.ts +1 -0
  57. package/build/esm/flagsmith-engine/features/util.js +3 -0
  58. package/build/esm/flagsmith-engine/index.d.ts +61 -9
  59. package/build/esm/flagsmith-engine/index.js +161 -43
  60. package/build/esm/flagsmith-engine/segments/constants.d.ts +1 -0
  61. package/build/esm/flagsmith-engine/segments/constants.js +1 -0
  62. package/build/esm/flagsmith-engine/segments/evaluators.d.ts +41 -7
  63. package/build/esm/flagsmith-engine/segments/evaluators.js +137 -25
  64. package/build/esm/flagsmith-engine/segments/models.d.ts +9 -4
  65. package/build/esm/flagsmith-engine/segments/models.js +115 -13
  66. package/build/esm/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  67. package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
  68. package/build/esm/sdk/index.d.ts +1 -3
  69. package/build/esm/sdk/index.js +21 -18
  70. package/build/esm/sdk/models.d.ts +8 -1
  71. package/build/esm/sdk/models.js +29 -1
  72. package/flagsmith-engine/environments/models.ts +3 -1
  73. package/flagsmith-engine/environments/util.ts +2 -1
  74. package/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +247 -0
  75. package/flagsmith-engine/evaluation/evaluationContext/mappers.ts +204 -0
  76. package/flagsmith-engine/evaluation/evaluationContext/types.ts +233 -0
  77. package/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +71 -0
  78. package/flagsmith-engine/evaluation/models.ts +96 -0
  79. package/flagsmith-engine/features/models.ts +3 -2
  80. package/flagsmith-engine/features/types.ts +5 -0
  81. package/flagsmith-engine/features/util.ts +4 -0
  82. package/flagsmith-engine/index.ts +229 -72
  83. package/flagsmith-engine/segments/constants.ts +1 -0
  84. package/flagsmith-engine/segments/evaluators.ts +178 -62
  85. package/flagsmith-engine/segments/models.ts +171 -23
  86. package/flagsmith-engine/utils/hashing/index.ts +2 -2
  87. package/package.json +13 -2
  88. package/sdk/index.ts +36 -23
  89. package/sdk/models.ts +44 -2
  90. package/tests/engine/e2e/engine.test.ts +43 -38
  91. package/tests/engine/unit/engine.test.ts +306 -59
  92. package/tests/engine/unit/mappers.test.ts +353 -0
  93. package/tests/engine/unit/segments/segment_evaluators.test.ts +391 -49
  94. package/tests/engine/unit/segments/segments_model.test.ts +85 -0
  95. package/tests/engine/unit/utils/utils.test.ts +7 -7
  96. package/tests/engine/unit/utils.ts +1 -1
  97. package/tests/sdk/data/environment.json +1 -0
  98. package/tests/sdk/flagsmith.test.ts +29 -3
  99. package/tests/sdk/offline-handlers.test.ts +3 -1
  100. package/vitest.config.esm.ts +34 -0
@@ -1,21 +1,24 @@
1
- import {
2
- ALL_RULE,
3
- CONDITION_OPERATORS,
4
- PERCENTAGE_SPLIT
5
- } from '../../../../flagsmith-engine/segments/constants.js';
6
- import { SegmentConditionModel } from '../../../../flagsmith-engine/segments/models.js';
1
+ import { ALL_RULE, CONDITION_OPERATORS } from '../../../../flagsmith-engine/segments/constants.js';
2
+
7
3
  import {
8
4
  traitsMatchSegmentCondition,
9
- evaluateIdentityInSegment
5
+ getContextValue,
6
+ getIdentitySegments
10
7
  } from '../../../../flagsmith-engine/segments/evaluators.js';
11
- import { TraitModel, IdentityModel } from '../../../../flagsmith-engine/index.js';
12
- import { environment } from '../utils.js';
13
- import { buildSegmentModel } from '../../../../flagsmith-engine/segments/util.js';
14
- import { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js';
8
+ import { TraitModel } from '../../../../flagsmith-engine/index.js';
9
+ import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js';
10
+ import {
11
+ EvaluationContext,
12
+ InSegmentCondition,
13
+ SegmentCondition,
14
+ SegmentCondition1
15
+ } from '../../../../flagsmith-engine/evaluation/models.js';
16
+
17
+ const isEsmBuild = process.env.ESM_BUILD === 'true';
15
18
 
16
19
  // todo: work out how to implement this in a test function or before hook
17
20
  vi.mock('../../../../flagsmith-engine/utils/hashing', () => ({
18
- getHashedPercentateForObjIds: vi.fn(() => 1)
21
+ getHashedPercentageForObjIds: vi.fn(() => 1)
19
22
  }));
20
23
 
21
24
  let traitExistenceTestCases: [
@@ -48,49 +51,388 @@ let traitExistenceTestCases: [
48
51
  test('test_traits_match_segment_condition_for_trait_existence_operators', () => {
49
52
  for (const testCase of traitExistenceTestCases) {
50
53
  const [operator, conditionProperty, conditionValue, traits, expectedResult] = testCase;
51
- let segmentModel = new SegmentConditionModel(operator, conditionValue, conditionProperty);
52
- expect(traitsMatchSegmentCondition(traits, segmentModel, 'any', 'any')).toBe(
53
- expectedResult
54
- );
54
+ let segmentConditionModel = {
55
+ operator,
56
+ value: conditionValue,
57
+ property: conditionProperty
58
+ };
59
+ const traitsMap = traits.reduce((acc, trait) => {
60
+ acc[trait.traitKey] = trait.traitValue;
61
+ return acc;
62
+ }, {});
63
+ const context: EvaluationContext = {
64
+ environment: {
65
+ key: 'any',
66
+ name: 'any'
67
+ },
68
+ identity: {
69
+ traits: traitsMap,
70
+ key: 'any',
71
+ identifier: 'any'
72
+ }
73
+ };
74
+ expect(
75
+ traitsMatchSegmentCondition(segmentConditionModel as SegmentCondition, 'any', context)
76
+ ).toBe(expectedResult);
55
77
  }
56
78
  });
57
79
 
58
- test('evaluateIdentityInSegment uses django ID for hashed percentage when present', () => {
59
- var identityModel = new IdentityModel(
60
- Date.now().toString(),
61
- [],
62
- [],
63
- environment().apiKey,
64
- 'identity_1',
65
- undefined,
66
- 1
67
- );
68
- const segmentDefinition = {
69
- id: 1,
70
- name: 'percentage_split_segment',
71
- rules: [
80
+ describe('getIdentitySegments integration', () => {
81
+ test('returns only matching segments', () => {
82
+ const context: EvaluationContext = {
83
+ environment: { key: 'env', name: 'test' },
84
+ identity: {
85
+ key: 'user',
86
+ identifier: 'premium@example.com',
87
+ traits: { subscription: 'premium' }
88
+ },
89
+ segments: {
90
+ '1': {
91
+ key: '1',
92
+ name: 'premium_users',
93
+ rules: [
94
+ {
95
+ type: 'ALL',
96
+ conditions: [
97
+ { property: 'subscription', operator: 'EQUAL', value: 'premium' }
98
+ ]
99
+ }
100
+ ],
101
+ overrides: []
102
+ },
103
+ '2': {
104
+ key: '2',
105
+ name: 'basic_users',
106
+ rules: [
107
+ {
108
+ type: 'ALL',
109
+ conditions: [
110
+ { property: 'subscription', operator: 'EQUAL', value: 'basic' }
111
+ ]
112
+ }
113
+ ],
114
+ overrides: []
115
+ }
116
+ },
117
+ features: {}
118
+ };
119
+
120
+ const result = getIdentitySegments(context);
121
+
122
+ expect(result).toHaveLength(1);
123
+ expect(result[0].name).toBe('premium_users');
124
+ });
125
+
126
+ test('returns empty array when no segments match', () => {
127
+ const context: EvaluationContext = {
128
+ environment: { key: 'env', name: 'test' },
129
+ identity: {
130
+ key: 'user',
131
+ identifier: 'test@example.com',
132
+ traits: { subscription: 'free' }
133
+ },
134
+ segments: {
135
+ '1': {
136
+ key: '1',
137
+ name: 'premium_users',
138
+ rules: [
139
+ {
140
+ type: 'ALL',
141
+ conditions: [
142
+ { property: 'subscription', operator: 'EQUAL', value: 'premium' }
143
+ ]
144
+ }
145
+ ],
146
+ overrides: []
147
+ }
148
+ },
149
+ features: {}
150
+ };
151
+
152
+ const result = getIdentitySegments(context);
153
+ expect(result).toEqual([]);
154
+ });
155
+ });
156
+
157
+ describe('IN operator', () => {
158
+ const mockContext: EvaluationContext = {
159
+ environment: { key: 'env', name: 'test' },
160
+ identity: {
161
+ key: 'test-user',
162
+ identifier: 'test',
163
+ traits: { name: 'test' }
164
+ },
165
+ segments: {},
166
+ features: {}
167
+ };
168
+
169
+ test.each([
170
+ // Array of strings
171
+ [
72
172
  {
73
- type: ALL_RULE,
74
- conditions: [
75
- {
76
- operator: PERCENTAGE_SPLIT,
77
- property_: null,
78
- value: '10'
79
- }
80
- ],
81
- rules: []
82
- }
173
+ property: '$.identity.identifier',
174
+ operator: CONDITION_OPERATORS.IN,
175
+ value: ['test', 'john-doe']
176
+ },
177
+ true
178
+ ],
179
+ [
180
+ {
181
+ property: '$.identity.identifier',
182
+ operator: CONDITION_OPERATORS.IN,
183
+ value: ['john-doe']
184
+ },
185
+ false
186
+ ],
187
+
188
+ // JSON encoded
189
+ [
190
+ {
191
+ property: '$.identity.identifier',
192
+ operator: CONDITION_OPERATORS.IN,
193
+ value: '["test", "john-doe"]'
194
+ },
195
+ true
83
196
  ],
84
- feature_states: []
197
+ [
198
+ {
199
+ property: '$.identity.identifier',
200
+ operator: CONDITION_OPERATORS.IN,
201
+ value: '["john-doe"]'
202
+ },
203
+ false
204
+ ],
205
+
206
+ // Legacy value string to split
207
+ [
208
+ {
209
+ property: '$.identity.identifier',
210
+ operator: CONDITION_OPERATORS.IN,
211
+ value: 'test,john-doe'
212
+ },
213
+ true
214
+ ],
215
+ [
216
+ {
217
+ property: '$.identity.identifier',
218
+ operator: CONDITION_OPERATORS.IN,
219
+ value: 'john-doe'
220
+ },
221
+ false
222
+ ],
223
+ // Fails because the value is split in middle
224
+ [
225
+ {
226
+ property: '$.identity.identifier',
227
+ operator: CONDITION_OPERATORS.IN,
228
+ value: 'te,st,john-doe'
229
+ },
230
+ false
231
+ ],
232
+
233
+ // Edge cases
234
+ [{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '' }, false],
235
+ [{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: [] }, false],
236
+ [
237
+ { property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '[]' },
238
+ false
239
+ ]
240
+ ] as Array<[SegmentCondition | InSegmentCondition, boolean]>)(
241
+ 'evaluates IN condition %j to %s',
242
+ (condition: SegmentCondition | InSegmentCondition, expected: boolean) => {
243
+ const result = traitsMatchSegmentCondition(condition, 'segment', mockContext);
244
+ expect(result).toBe(expected);
245
+ }
246
+ );
247
+ });
248
+
249
+ describe('getIdentitySegments single segment evaluation', () => {
250
+ const baseContext: EvaluationContext = {
251
+ environment: { key: 'env', name: 'test' },
252
+ identity: { key: 'user', identifier: 'test@example.com', traits: { age: 25 } },
253
+ segments: {},
254
+ features: {}
255
+ };
256
+
257
+ test('returns empty array for segment with no rules', () => {
258
+ const context = {
259
+ ...baseContext,
260
+ segments: {
261
+ '1': {
262
+ key: '1',
263
+ name: 'empty_segment',
264
+ rules: [],
265
+ overrides: []
266
+ }
267
+ }
268
+ };
269
+
270
+ expect(getIdentitySegments(context)).toEqual([]);
271
+ });
272
+
273
+ test('returns segment when all rules match', () => {
274
+ const context: EvaluationContext = {
275
+ ...baseContext,
276
+ segments: {
277
+ '1': {
278
+ key: '1',
279
+ name: 'matching_segment',
280
+ rules: [
281
+ {
282
+ type: ALL_RULE,
283
+ conditions: [
284
+ {
285
+ property: '$.identity.identifier',
286
+ operator: 'EQUAL',
287
+ value: 'test@example.com'
288
+ }
289
+ ],
290
+ rules: []
291
+ },
292
+ {
293
+ type: ALL_RULE,
294
+ conditions: [
295
+ {
296
+ property: '$.identity.identifier',
297
+ operator: 'CONTAINS',
298
+ value: 'test@example.com'
299
+ }
300
+ ],
301
+ rules: []
302
+ }
303
+ ],
304
+ overrides: []
305
+ }
306
+ }
307
+ };
308
+
309
+ const result = getIdentitySegments(context);
310
+ expect(result).toHaveLength(1);
311
+ expect(result[0].name).toBe('matching_segment');
312
+ });
313
+
314
+ test('returns empty array when any rule fails', () => {
315
+ const context: EvaluationContext = {
316
+ ...baseContext,
317
+ segments: {
318
+ '1': {
319
+ key: '1',
320
+ name: 'failing_segment',
321
+ rules: [
322
+ {
323
+ type: ALL_RULE,
324
+ conditions: [
325
+ {
326
+ property: '$.identity.identifier',
327
+ operator: 'EQUAL',
328
+ value: 'test@example.com'
329
+ }
330
+ ],
331
+ rules: []
332
+ },
333
+ {
334
+ type: ALL_RULE,
335
+ conditions: [{ property: 'age', operator: 'EQUAL', value: '30' }],
336
+ rules: []
337
+ }
338
+ ],
339
+ overrides: []
340
+ }
341
+ }
342
+ };
343
+
344
+ expect(getIdentitySegments(context)).toEqual([]);
345
+ });
346
+ });
347
+
348
+ describe('getContextValue', () => {
349
+ const mockContext: EvaluationContext = {
350
+ environment: {
351
+ key: 'test-env-key',
352
+ name: 'Test Environment'
353
+ },
354
+ identity: {
355
+ key: 'user-123',
356
+ identifier: 'user@example.com'
357
+ },
358
+ segments: {},
359
+ features: {}
360
+ };
361
+
362
+ // Success cases
363
+ test.each([
364
+ ['$.identity.identifier', 'user@example.com'],
365
+ ['$.environment.name', 'Test Environment'],
366
+ ['$.environment.key', 'test-env-key']
367
+ ])('returns correct value for path %s', (jsonPath, expected) => {
368
+ const result = getContextValue(jsonPath, mockContext);
369
+ expect(result).toBe(expected);
370
+ });
371
+
372
+ // Undefined or invalid cases
373
+ test.each([
374
+ ['$.identity.traits.user_type', 'unsupported nested path'],
375
+ ['identity.identifier', 'missing $ prefix'],
376
+ ['$.invalid.path', 'completely invalid path'],
377
+ ['$.identity.nonexistent', 'valid structure but missing property'],
378
+ ['', 'empty string'],
379
+ ['$', 'just $ symbol']
380
+ ])('returns undefined for %s (%s)', jsonPath => {
381
+ const result = getContextValue(jsonPath, mockContext);
382
+ expect(result).toBeUndefined();
383
+ });
384
+
385
+ // Context error cases
386
+ test.each([
387
+ [undefined, '$.identity.identifier', 'undefined context'],
388
+ [{ segments: {}, features: {} }, '$.identity.identifier', 'missing identity'],
389
+ [
390
+ { identity: { key: 'test', identifier: 'test' }, segments: {}, features: {} },
391
+ '$.environment.name',
392
+ 'missing environment'
393
+ ]
394
+ ])('returns undefined when %s', (context, jsonPath, _) => {
395
+ const result = getContextValue(jsonPath, context as EvaluationContext);
396
+ expect(result).toBeUndefined();
397
+ });
398
+ });
399
+
400
+ // Skip in ESM build: vi.mock doesn't work with external modules
401
+ describe.skipIf(isEsmBuild)('percentage split operator', () => {
402
+ const mockContext: EvaluationContext = {
403
+ environment: { key: 'env', name: 'Test Env' },
404
+ identity: {
405
+ key: 'user-123',
406
+ identifier: 'test@example.com',
407
+ traits: {
408
+ age: 25,
409
+ subscription: 'premium',
410
+ active: true
411
+ }
412
+ },
413
+ segments: {},
414
+ features: {}
85
415
  };
86
- const segmentModel = buildSegmentModel(segmentDefinition);
416
+ beforeEach(() => {
417
+ vi.clearAllMocks();
418
+ });
87
419
 
88
- var result = evaluateIdentityInSegment(identityModel, segmentModel);
420
+ test.each([
421
+ [25.5, 30, true],
422
+ [25.5, 20, false],
423
+ [25.5, 25.5, true],
424
+ [0, 0, true],
425
+ [100, 99.9, false]
426
+ ])('percentage %d with threshold %d returns %s', (hashedValue, threshold, expected) => {
427
+ const mockHashFn = getHashedPercentageForObjIds;
428
+ mockHashFn.mockReturnValue(hashedValue);
429
+ const condition = {
430
+ operator: 'PERCENTAGE_SPLIT',
431
+ value: threshold.toString()
432
+ } as SegmentCondition1 | InSegmentCondition;
433
+ const result = traitsMatchSegmentCondition(condition, 'seg1', mockContext);
89
434
 
90
- expect(result).toBe(true);
91
- expect(getHashedPercentateForObjIds).toHaveBeenCalledTimes(1);
92
- expect(getHashedPercentateForObjIds).toHaveBeenCalledWith([
93
- segmentModel.id,
94
- identityModel.djangoID
95
- ]);
435
+ expect(result).toBe(expected);
436
+ expect(getHashedPercentageForObjIds).toHaveBeenCalledWith(['seg1', 'user-123']);
437
+ });
96
438
  });
@@ -1,3 +1,6 @@
1
+ import { SegmentSource } from '../../../../flagsmith-engine/evaluation/models';
2
+ import { EvaluationContext } from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types';
3
+ import { CONSTANTS } from '../../../../flagsmith-engine/features/constants';
1
4
  import {
2
5
  ALL_RULE,
3
6
  ANY_RULE,
@@ -8,6 +11,7 @@ import {
8
11
  all,
9
12
  any,
10
13
  SegmentConditionModel,
14
+ SegmentModel,
11
15
  SegmentRuleModel
12
16
  } from '../../../../flagsmith-engine/segments/models';
13
17
 
@@ -135,3 +139,84 @@ test('test_segment_rule_matching_function', () => {
135
139
  expect(new SegmentRuleModel(testCase[0]).matchingFunction()).toBe(testCase[1]);
136
140
  }
137
141
  });
142
+
143
+ test('test_fromSegmentResult_with_multiple_variants', () => {
144
+ const segmentResults = [{ name: 'test_segment', metadata: { id: '1' } }];
145
+
146
+ const evaluationContext: EvaluationContext = {
147
+ identity: {
148
+ key: 'not_exist',
149
+ identifier: 'not_exist'
150
+ },
151
+ environment: {
152
+ key: 'test',
153
+ name: 'test'
154
+ },
155
+ features: {},
156
+ segments: {
157
+ '1': {
158
+ key: '1',
159
+ name: 'test_segment',
160
+ metadata: {
161
+ source: SegmentSource.API,
162
+ id: '1'
163
+ },
164
+ rules: [
165
+ {
166
+ type: 'ALL',
167
+ conditions: [
168
+ {
169
+ property: '$.identity.identifier',
170
+ operator: 'EQUAL',
171
+ value: 'test-user'
172
+ }
173
+ ]
174
+ }
175
+ ],
176
+ overrides: [
177
+ {
178
+ key: 'override',
179
+ name: 'multivariate_feature',
180
+ enabled: true,
181
+ value: 'default_value',
182
+ priority: 1,
183
+ metadata: {
184
+ id: 1
185
+ },
186
+ variants: [
187
+ { id: 1, value: 'variant_a', weight: 30 },
188
+ { id: 2, value: 'variant_b', weight: 70 }
189
+ ]
190
+ }
191
+ ]
192
+ }
193
+ }
194
+ };
195
+
196
+ const result = SegmentModel.fromSegmentResult(segmentResults, evaluationContext);
197
+
198
+ expect(result).toHaveLength(1);
199
+
200
+ const segment = result[0];
201
+ expect(segment.name).toBe('test_segment');
202
+ expect(segment.featureStates).toHaveLength(1);
203
+
204
+ const featureState = segment.featureStates[0];
205
+ expect(featureState.feature.name).toBe('multivariate_feature');
206
+ expect(featureState.feature.type).toBe(CONSTANTS.MULTIVARIATE);
207
+ expect(featureState.enabled).toBe(true);
208
+ expect(featureState.getValue()).toBe('default_value');
209
+
210
+ // Test multivariate variants
211
+ expect(featureState.multivariateFeatureStateValues).toHaveLength(2);
212
+
213
+ const variant1 = featureState.multivariateFeatureStateValues[0];
214
+ expect(variant1.multivariateFeatureOption.value).toBe('variant_a');
215
+ expect(variant1.percentageAllocation).toBe(30);
216
+ expect(variant1.id).toBe(1);
217
+
218
+ const variant2 = featureState.multivariateFeatureStateValues[1];
219
+ expect(variant2.multivariateFeatureOption.value).toBe('variant_b');
220
+ expect(variant2.percentageAllocation).toBe(70);
221
+ expect(variant2.id).toBe(2);
222
+ });
@@ -1,11 +1,11 @@
1
1
  import { randomUUID as uuidv4 } from 'node:crypto';
2
- import { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js';
2
+ import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js';
3
3
 
4
4
  describe('getHashedPercentageForObjIds', () => {
5
5
  it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])(
6
6
  'returns x where 0 <= x < 100',
7
7
  (objIds: (string | number)[]) => {
8
- let result = getHashedPercentateForObjIds(objIds);
8
+ let result = getHashedPercentageForObjIds(objIds);
9
9
  expect(result).toBeLessThan(100);
10
10
  expect(result).toBeGreaterThanOrEqual(0);
11
11
  }
@@ -14,15 +14,15 @@ describe('getHashedPercentageForObjIds', () => {
14
14
  it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])(
15
15
  'returns the same value each time',
16
16
  (objIds: (string | number)[]) => {
17
- let resultOne = getHashedPercentateForObjIds(objIds);
18
- let resultTwo = getHashedPercentateForObjIds(objIds);
17
+ let resultOne = getHashedPercentageForObjIds(objIds);
18
+ let resultTwo = getHashedPercentageForObjIds(objIds);
19
19
  expect(resultOne).toEqual(resultTwo);
20
20
  }
21
21
  );
22
22
 
23
23
  it('is unique for different object ids', () => {
24
- let resultOne = getHashedPercentateForObjIds([14, 106]);
25
- let resultTwo = getHashedPercentateForObjIds([53, 200]);
24
+ let resultOne = getHashedPercentageForObjIds([14, 106]);
25
+ let resultTwo = getHashedPercentageForObjIds([53, 200]);
26
26
  expect(resultOne).not.toEqual(resultTwo);
27
27
  });
28
28
 
@@ -40,7 +40,7 @@ describe('getHashedPercentageForObjIds', () => {
40
40
  );
41
41
 
42
42
  // When
43
- let values = objectIdPairs.map(objIds => getHashedPercentateForObjIds(objIds));
43
+ let values = objectIdPairs.map(objIds => getHashedPercentageForObjIds(objIds));
44
44
 
45
45
  // Then
46
46
  for (let i = 0; i++; i < numTestBuckets) {
@@ -20,7 +20,7 @@ export function segmentCondition() {
20
20
  }
21
21
 
22
22
  export function traitMatchingSegment() {
23
- return new TraitModel(segmentCondition().property_ as string, segmentCondition().value);
23
+ return new TraitModel(segmentCondition().property as string, segmentCondition().value);
24
24
  }
25
25
 
26
26
  export function organisation() {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "api_key": "B62qaMZNwfiqT76p38ggrQ",
3
+ "name": "Test environment",
3
4
  "project": {
4
5
  "name": "Test project",
5
6
  "organisation": {
@@ -14,8 +14,12 @@ import { EnvironmentModel } from '../../flagsmith-engine/environments/models.js'
14
14
  import { BaseOfflineHandler } from '../../sdk/offline_handlers.js';
15
15
  import { Agent } from 'undici';
16
16
 
17
+ const isEsmBuild = process.env.ESM_BUILD === 'true';
18
+
17
19
  vi.mock('../../sdk/polling_manager');
18
- test('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => {
20
+
21
+ // Skip in ESM build: vi.mock doesn't work with external modules
22
+ test.skipIf(isEsmBuild)('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => {
19
23
  new Flagsmith({
20
24
  environmentKey: 'ser.key',
21
25
  enableLocalEvaluation: true
@@ -32,7 +36,8 @@ test('test_flagsmith_local_evaluation_key_required', () => {
32
36
  }).toThrow('Using local evaluation requires a server-side environment key');
33
37
  });
34
38
 
35
- test('test_update_environment_sets_environment', async () => {
39
+ // Skip in ESM build: instanceof fails across module boundaries
40
+ test.skipIf(isEsmBuild)('test_update_environment_sets_environment', async () => {
36
41
  const flg = flagsmith({
37
42
  environmentKey: 'ser.key'
38
43
  });
@@ -513,9 +518,30 @@ test('getIdentityFlags succeeds if initial fetch failed then succeeded', async (
513
518
  expect(flags2.isFeatureEnabled('some_feature')).toBe(true);
514
519
  });
515
520
 
516
- test('get_user_agent_extracts_version_from_package_json', async () => {
521
+ // Skip in ESM build: require() path resolution differs
522
+ test.skipIf(isEsmBuild)('get_user_agent_extracts_version_from_package_json', async () => {
517
523
  const userAgent = getUserAgent();
518
524
  const packageJson = require('../../package.json');
519
525
 
520
526
  expect(userAgent).toBe(`flagsmith-nodejs-sdk/${packageJson.version}`);
521
527
  });
528
+
529
+ test('Flags.fromEvaluationResult throws error when metadata.id is missing', () => {
530
+ const evaluationResult = {
531
+ flags: {
532
+ test_feature: {
533
+ enabled: true,
534
+ name: 'test_feature',
535
+ value: 'test_value',
536
+ reason: 'DEFAULT',
537
+ metadata: {}
538
+ }
539
+ },
540
+ segments: []
541
+ };
542
+
543
+ expect(() => Flags.fromEvaluationResult(evaluationResult as any)).toThrow(
544
+ 'FlagResult metadata.id is missing for feature "test_feature". ' +
545
+ 'This indicates a bug in the SDK, please report it.'
546
+ );
547
+ });