@featurevisor/core 2.20.0 → 2.22.1

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 (87) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/coverage/clover.xml +1286 -515
  3. package/coverage/coverage-final.json +17 -16
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildDatafile.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  8. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  9. package/coverage/lcov-report/builder/convertToV1.ts.html +1 -1
  10. package/coverage/lcov-report/builder/getFeatureRanges.ts.html +1 -1
  11. package/coverage/lcov-report/builder/hashes.ts.html +1 -1
  12. package/coverage/lcov-report/builder/index.html +1 -1
  13. package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
  14. package/coverage/lcov-report/builder/mutator.ts.html +1 -1
  15. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  16. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  17. package/coverage/lcov-report/config/index.html +1 -1
  18. package/coverage/lcov-report/config/index.ts.html +1 -1
  19. package/coverage/lcov-report/config/projectConfig.ts.html +42 -42
  20. package/coverage/lcov-report/datasource/adapter.ts.html +2 -2
  21. package/coverage/lcov-report/datasource/datasource.ts.html +43 -43
  22. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +48 -48
  23. package/coverage/lcov-report/datasource/index.html +19 -19
  24. package/coverage/lcov-report/datasource/index.ts.html +4 -4
  25. package/coverage/lcov-report/generate-code/index.html +116 -0
  26. package/coverage/lcov-report/generate-code/typescript.ts.html +3004 -0
  27. package/coverage/lcov-report/index.html +44 -29
  28. package/coverage/lcov-report/linter/attributeSchema.ts.html +1815 -30
  29. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
  30. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
  31. package/coverage/lcov-report/linter/conditionSchema.ts.html +1606 -157
  32. package/coverage/lcov-report/linter/featureSchema.ts.html +56 -56
  33. package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
  34. package/coverage/lcov-report/linter/index.html +39 -39
  35. package/coverage/lcov-report/linter/lintProject.ts.html +189 -78
  36. package/coverage/lcov-report/linter/mutationNotation.ts.html +1 -1
  37. package/coverage/lcov-report/linter/printError.ts.html +1 -1
  38. package/coverage/lcov-report/linter/schema.ts.html +59 -59
  39. package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
  40. package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
  41. package/coverage/lcov-report/list/index.html +1 -1
  42. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  43. package/coverage/lcov-report/parsers/index.html +1 -1
  44. package/coverage/lcov-report/parsers/index.ts.html +4 -4
  45. package/coverage/lcov-report/parsers/json.ts.html +2 -2
  46. package/coverage/lcov-report/parsers/yml.ts.html +6 -6
  47. package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
  48. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  49. package/coverage/lcov-report/tester/index.html +1 -1
  50. package/coverage/lcov-report/utils/extractKeys.ts.html +1 -1
  51. package/coverage/lcov-report/utils/git.ts.html +3 -3
  52. package/coverage/lcov-report/utils/index.html +1 -1
  53. package/coverage/lcov-report/utils/index.ts.html +1 -1
  54. package/coverage/lcov-report/utils/pretty.ts.html +1 -1
  55. package/coverage/lcov.info +2624 -926
  56. package/lib/generate-code/typescript.js +57 -29
  57. package/lib/generate-code/typescript.js.map +1 -1
  58. package/lib/generate-code/typescript.spec.d.ts +1 -0
  59. package/lib/generate-code/typescript.spec.js +78 -0
  60. package/lib/generate-code/typescript.spec.js.map +1 -0
  61. package/lib/linter/attributeSchema.d.ts +352 -20
  62. package/lib/linter/attributeSchema.js +426 -16
  63. package/lib/linter/attributeSchema.js.map +1 -1
  64. package/lib/linter/attributeSchema.spec.d.ts +1 -0
  65. package/lib/linter/attributeSchema.spec.js +266 -0
  66. package/lib/linter/attributeSchema.spec.js.map +1 -0
  67. package/lib/linter/conditionSchema.d.ts +2 -1
  68. package/lib/linter/conditionSchema.js +370 -100
  69. package/lib/linter/conditionSchema.js.map +1 -1
  70. package/lib/linter/conditionSchema.spec.js +200 -153
  71. package/lib/linter/conditionSchema.spec.js.map +1 -1
  72. package/lib/linter/featureSchema.spec.js +114 -3
  73. package/lib/linter/featureSchema.spec.js.map +1 -1
  74. package/lib/linter/lintProject.js +45 -14
  75. package/lib/linter/lintProject.js.map +1 -1
  76. package/lib/linter/segmentSchema.spec.js +71 -1
  77. package/lib/linter/segmentSchema.spec.js.map +1 -1
  78. package/package.json +5 -5
  79. package/src/generate-code/typescript.spec.ts +108 -0
  80. package/src/generate-code/typescript.ts +80 -34
  81. package/src/linter/attributeSchema.spec.ts +342 -0
  82. package/src/linter/attributeSchema.ts +613 -18
  83. package/src/linter/conditionSchema.spec.ts +231 -189
  84. package/src/linter/conditionSchema.ts +582 -99
  85. package/src/linter/featureSchema.spec.ts +129 -4
  86. package/src/linter/lintProject.ts +55 -18
  87. package/src/linter/segmentSchema.spec.ts +82 -1
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Unit tests for condition schema validation (segment conditions, feature rules, etc.).
3
- * Covers getConditionsZodSchema: attribute ref, operators, value type per operator,
4
- * regexFlags, and/or/not nesting, and "*" (everyone).
3
+ * Covers getConditionsZodSchema: attribute path resolution, operator/value compatibility,
4
+ * schema-aware value validation, regexFlags, nesting, and "*" (everyone).
5
5
  */
6
6
  import { z } from "zod";
7
7
 
8
+ import type { Attribute } from "@featurevisor/types";
8
9
  import type { ProjectConfig } from "../config";
9
10
  import { getConditionsZodSchema } from "./conditionSchema";
10
11
 
@@ -35,7 +36,80 @@ function minimalProjectConfig(): ProjectConfig {
35
36
  };
36
37
  }
37
38
 
38
- const TEST_ATTRIBUTES: [string, ...string[]] = ["userId", "country", "device", "email"];
39
+ const TEST_ATTRIBUTES: Record<string, Attribute> = {
40
+ userId: {
41
+ description: "User ID",
42
+ type: "string",
43
+ },
44
+ country: {
45
+ description: "Country",
46
+ type: "string",
47
+ enum: ["de", "fr", "nl", "us"],
48
+ },
49
+ device: {
50
+ description: "Device",
51
+ type: "string",
52
+ },
53
+ email: {
54
+ description: "Email",
55
+ type: "string",
56
+ pattern: "^[^@]+@[^@]+\\.[^@]+$",
57
+ },
58
+ age: {
59
+ description: "Age",
60
+ type: "integer",
61
+ minimum: 18,
62
+ },
63
+ browser: {
64
+ description: "Browser",
65
+ type: "object",
66
+ properties: {
67
+ name: {
68
+ type: "string",
69
+ enum: ["chrome", "firefox", "safari"],
70
+ },
71
+ version: {
72
+ type: "semver",
73
+ },
74
+ },
75
+ },
76
+ account: {
77
+ description: "Account",
78
+ type: "object",
79
+ properties: {
80
+ plan: {
81
+ type: "string",
82
+ enum: ["free", "pro"],
83
+ },
84
+ locale: {
85
+ type: "string",
86
+ },
87
+ },
88
+ },
89
+ labels: {
90
+ description: "Labels",
91
+ type: "object",
92
+ additionalProperties: {
93
+ type: "string",
94
+ },
95
+ },
96
+ permissions: {
97
+ description: "Permissions",
98
+ type: "array",
99
+ items: {
100
+ type: "string",
101
+ enum: ["read", "write", "admin"],
102
+ },
103
+ },
104
+ version: {
105
+ description: "Version",
106
+ oneOf: [{ type: "string" }, { type: "double" }],
107
+ },
108
+ traits: {
109
+ description: "Traits",
110
+ type: "object",
111
+ },
112
+ };
39
113
 
40
114
  function getConditionsSchema() {
41
115
  return getConditionsZodSchema(minimalProjectConfig(), TEST_ATTRIBUTES);
@@ -69,7 +143,6 @@ function expectConditionsFailure(input: unknown, messageSubstring?: string): z.Z
69
143
  return err;
70
144
  }
71
145
 
72
- /** Assert that an intentional mistake produces an error at the expected path with expected message. */
73
146
  function expectConditionErrorSurfaces(
74
147
  input: unknown,
75
148
  opts: { pathContains: string[]; messageContains: string },
@@ -83,16 +156,36 @@ function expectConditionErrorSurfaces(
83
156
  }
84
157
 
85
158
  describe("conditionSchema.ts :: getConditionsZodSchema", () => {
86
- describe("attribute", () => {
87
- it("accepts condition when attribute is in available list", () => {
159
+ describe("attribute path resolution", () => {
160
+ it("accepts direct attributes", () => {
161
+ expectConditionsSuccess({ attribute: "userId", operator: "equals", value: "u1" });
162
+ });
163
+
164
+ it("accepts nested attributes from declared object properties", () => {
88
165
  expectConditionsSuccess({
89
- attribute: "userId",
166
+ attribute: "browser.name",
167
+ operator: "equals",
168
+ value: "chrome",
169
+ });
170
+ });
171
+
172
+ it("accepts dynamic nested attributes via additionalProperties", () => {
173
+ expectConditionsSuccess({
174
+ attribute: "labels.locale",
90
175
  operator: "equals",
91
- value: "u1",
176
+ value: "nl-NL",
92
177
  });
93
178
  });
94
179
 
95
- it("rejects condition when attribute is unknown", () => {
180
+ it("accepts one-level nested attributes on flat objects", () => {
181
+ expectConditionsSuccess({
182
+ attribute: "traits.favoriteColor",
183
+ operator: "equals",
184
+ value: "blue",
185
+ });
186
+ });
187
+
188
+ it("rejects unknown attributes", () => {
96
189
  expectConditionsFailure(
97
190
  {
98
191
  attribute: "unknownAttr",
@@ -102,60 +195,70 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
102
195
  "Unknown attribute",
103
196
  );
104
197
  });
198
+
199
+ it("rejects paths that are too deep for flat objects", () => {
200
+ expectConditionsFailure(
201
+ {
202
+ attribute: "traits.preferences.theme",
203
+ operator: "equals",
204
+ value: "dark",
205
+ },
206
+ "Unknown attribute",
207
+ );
208
+ });
105
209
  });
106
210
 
107
- describe("operator and value: common (equals, notEquals)", () => {
108
- it("accepts string value with equals", () => {
109
- expectConditionsSuccess({ attribute: "userId", operator: "equals", value: "u1" });
211
+ describe("operator and value validation", () => {
212
+ it("accepts schema-aware equality checks", () => {
213
+ expectConditionsSuccess({
214
+ attribute: "browser.name",
215
+ operator: "equals",
216
+ value: "firefox",
217
+ });
110
218
  });
111
219
 
112
- it("accepts number value with equals", () => {
113
- expectConditionsSuccess({ attribute: "userId", operator: "equals", value: 42 });
220
+ it("rejects equality values outside enum constraints", () => {
221
+ expectConditionsFailure(
222
+ {
223
+ attribute: "browser.name",
224
+ operator: "equals",
225
+ value: "edge",
226
+ },
227
+ 'Value does not match the schema of attribute "browser.name"',
228
+ );
114
229
  });
115
230
 
116
- it("accepts boolean value with equals", () => {
117
- expectConditionsSuccess({ attribute: "device", operator: "equals", value: true });
231
+ it("accepts number equality on numeric attributes", () => {
232
+ expectConditionsSuccess({ attribute: "age", operator: "equals", value: 42 });
118
233
  });
119
234
 
120
- it("accepts null value with equals", () => {
235
+ it("accepts null equality checks", () => {
121
236
  expectConditionsSuccess({ attribute: "userId", operator: "equals", value: null });
122
237
  });
123
238
 
124
- it("rejects array value with equals", () => {
239
+ it("rejects array values with equals", () => {
125
240
  expectConditionsFailure(
126
241
  { attribute: "userId", operator: "equals", value: ["a", "b"] },
127
242
  "value has to be",
128
243
  );
129
244
  });
130
- });
131
245
 
132
- describe("operator and value: numeric", () => {
133
- it("accepts number value with greaterThan", () => {
246
+ it("accepts numeric operators on numeric attributes", () => {
134
247
  expectConditionsSuccess({
135
- attribute: "userId",
248
+ attribute: "age",
136
249
  operator: "greaterThan",
137
- value: 10,
250
+ value: 21,
138
251
  });
139
252
  });
140
253
 
141
- it("rejects string value with greaterThan", () => {
254
+ it("rejects numeric operators on string attributes", () => {
142
255
  expectConditionsFailure(
143
- { attribute: "userId", operator: "greaterThan", value: "10" },
144
- "value must be a number",
256
+ { attribute: "country", operator: "greaterThan", value: 10 },
257
+ "can only be used with integer or double attributes",
145
258
  );
146
259
  });
147
260
 
148
- it("rejects missing value with lessThan", () => {
149
- const result = parseConditions({
150
- attribute: "userId",
151
- operator: "lessThan",
152
- });
153
- expect(result.success).toBe(false);
154
- });
155
- });
156
-
157
- describe("operator and value: string", () => {
158
- it("accepts string value with contains", () => {
261
+ it("accepts string operators on string-like attributes", () => {
159
262
  expectConditionsSuccess({
160
263
  attribute: "email",
161
264
  operator: "contains",
@@ -163,40 +266,55 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
163
266
  });
164
267
  });
165
268
 
166
- it("rejects number value with startsWith", () => {
269
+ it("rejects string operators on array attributes", () => {
167
270
  expectConditionsFailure(
168
- { attribute: "country", operator: "startsWith", value: 123 },
169
- "value must be a string",
271
+ { attribute: "permissions", operator: "contains", value: "admin" },
272
+ "can only be used with string-like attributes",
170
273
  );
171
274
  });
172
- });
173
275
 
174
- describe("operator and value: date", () => {
175
- it("accepts ISO 8601 string with before", () => {
276
+ it("accepts date operators with ISO strings", () => {
176
277
  expectConditionsSuccess({
177
- attribute: "userId",
278
+ attribute: "device",
178
279
  operator: "before",
179
280
  value: "2025-12-31T23:59:59Z",
180
281
  });
181
282
  });
182
283
 
183
- it("rejects non-ISO string with after", () => {
284
+ it("rejects date operators with invalid strings", () => {
184
285
  expectConditionsFailure(
185
- { attribute: "userId", operator: "after", value: "not-a-date" },
286
+ { attribute: "device", operator: "after", value: "not-a-date" },
186
287
  "ISO 8601",
187
288
  );
188
289
  });
189
290
 
190
- it("rejects missing value with before", () => {
191
- expectConditionsFailure(
192
- { attribute: "userId", operator: "before" },
193
- "value must be provided",
194
- );
291
+ it("accepts semver operators on semver attributes", () => {
292
+ expectConditionsSuccess({
293
+ attribute: "browser.version",
294
+ operator: "semverEquals",
295
+ value: "1.0.0",
296
+ });
297
+ });
298
+
299
+ it("accepts semver operators on oneOf attributes when a string branch supports them", () => {
300
+ expectConditionsSuccess({
301
+ attribute: "version",
302
+ operator: "semverGreaterThan",
303
+ value: "5.0.0",
304
+ });
305
+ });
306
+
307
+ it("accepts numeric equality on oneOf attributes when a numeric branch matches", () => {
308
+ expectConditionsSuccess({
309
+ attribute: "version",
310
+ operator: "equals",
311
+ value: 5.5,
312
+ });
195
313
  });
196
314
  });
197
315
 
198
- describe("operator and value: array (in, notIn)", () => {
199
- it("accepts array of strings with in", () => {
316
+ describe("array-aware operators", () => {
317
+ it("accepts in/notIn on scalar attributes", () => {
200
318
  expectConditionsSuccess({
201
319
  attribute: "country",
202
320
  operator: "in",
@@ -204,28 +322,57 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
204
322
  });
205
323
  });
206
324
 
207
- it("rejects non-array value with notIn", () => {
325
+ it("rejects non-array values for in/notIn", () => {
208
326
  expectConditionsFailure(
209
327
  { attribute: "country", operator: "notIn", value: "de" },
210
328
  "value must be an array",
211
329
  );
212
330
  });
213
- });
214
331
 
215
- describe("operator and value: regex (matches, notMatches)", () => {
216
- it("accepts string value with matches", () => {
332
+ it("rejects in/notIn members that violate attribute schema", () => {
333
+ expectConditionsFailure(
334
+ { attribute: "country", operator: "in", value: ["de", "unknown"] },
335
+ 'Value at index 1 does not match the schema of attribute "country"',
336
+ );
337
+ });
338
+
339
+ it("accepts includes for arrays with typed string items", () => {
217
340
  expectConditionsSuccess({
218
- attribute: "email",
219
- operator: "matches",
220
- value: "^[a-z]+@",
341
+ attribute: "permissions",
342
+ operator: "includes",
343
+ value: "admin",
221
344
  });
222
345
  });
223
346
 
347
+ it("rejects includes values outside item enum constraints", () => {
348
+ expectConditionsFailure(
349
+ {
350
+ attribute: "permissions",
351
+ operator: "includes",
352
+ value: "delete",
353
+ },
354
+ 'Value does not match the item schema of attribute "permissions"',
355
+ );
356
+ });
357
+
358
+ it("rejects non-string includes values for string arrays", () => {
359
+ expectConditionsFailure(
360
+ {
361
+ attribute: "permissions",
362
+ operator: "includes",
363
+ value: 42,
364
+ },
365
+ 'Value does not match the item schema of attribute "permissions"',
366
+ );
367
+ });
368
+ });
369
+
370
+ describe("regex flags", () => {
224
371
  it("accepts regexFlags with matches", () => {
225
372
  expectConditionsSuccess({
226
- attribute: "email",
373
+ attribute: "browser.version",
227
374
  operator: "matches",
228
- value: "hello",
375
+ value: "1\\.0\\.0",
229
376
  regexFlags: "i",
230
377
  });
231
378
  });
@@ -233,7 +380,7 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
233
380
  it("rejects invalid regexFlags", () => {
234
381
  expectConditionsFailure(
235
382
  {
236
- attribute: "email",
383
+ attribute: "browser.version",
237
384
  operator: "matches",
238
385
  value: "x",
239
386
  regexFlags: "invalid",
@@ -255,15 +402,15 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
255
402
  });
256
403
  });
257
404
 
258
- describe("operator: exists, notExists (no value)", () => {
259
- it("accepts condition without value for exists", () => {
405
+ describe("exists/notExists", () => {
406
+ it("accepts exists without value", () => {
260
407
  expectConditionsSuccess({
261
408
  attribute: "userId",
262
409
  operator: "exists",
263
410
  });
264
411
  });
265
412
 
266
- it("rejects value when operator is exists", () => {
413
+ it("rejects exists with value", () => {
267
414
  expectConditionsFailure(
268
415
  { attribute: "userId", operator: "exists", value: "x" },
269
416
  "value is not needed",
@@ -271,94 +418,45 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
271
418
  });
272
419
  });
273
420
 
274
- describe("structure: and / or / not", () => {
275
- it("accepts and array of conditions", () => {
276
- expectConditionsSuccess({
277
- and: [
278
- { attribute: "userId", operator: "equals", value: "u1" },
279
- { attribute: "country", operator: "equals", value: "de" },
280
- ],
281
- });
282
- });
283
-
284
- it("accepts or array of conditions", () => {
285
- expectConditionsSuccess({
286
- or: [
287
- { attribute: "country", operator: "equals", value: "de" },
288
- { attribute: "country", operator: "equals", value: "fr" },
289
- ],
290
- });
291
- });
292
-
293
- it("accepts not array of conditions", () => {
294
- expectConditionsSuccess({
295
- not: [{ attribute: "country", operator: "equals", value: "us" }],
296
- });
297
- });
298
-
299
- it("accepts nested and inside or", () => {
421
+ describe("structure", () => {
422
+ it("accepts nested and/or/not conditions", () => {
300
423
  expectConditionsSuccess({
301
424
  or: [
302
425
  {
303
426
  and: [
304
- { attribute: "userId", operator: "equals", value: "u1" },
305
- { attribute: "device", operator: "equals", value: "mobile" },
427
+ { attribute: "browser.name", operator: "equals", value: "chrome" },
428
+ { attribute: "country", operator: "equals", value: "de" },
306
429
  ],
307
430
  },
308
- { attribute: "country", operator: "equals", value: "de" },
431
+ { attribute: "permissions", operator: "includes", value: "admin" },
309
432
  ],
310
433
  });
311
434
  });
312
435
 
313
- it("rejects unknown attribute inside nested condition", () => {
436
+ it("rejects invalid nested conditions", () => {
314
437
  expectConditionsFailure(
315
438
  {
316
439
  and: [
317
440
  { attribute: "userId", operator: "equals", value: "u1" },
318
- { attribute: "badAttr", operator: "equals", value: "x" },
441
+ { attribute: "browser.name", operator: "equals", value: "edge" },
319
442
  ],
320
443
  },
321
- "Unknown attribute",
322
- );
323
- });
324
-
325
- it("rejects invalid value type inside nested condition", () => {
326
- expectConditionsFailure(
327
- {
328
- or: [{ attribute: "country", operator: "greaterThan", value: "string" }],
329
- },
330
- "value must be a number",
444
+ 'Value does not match the schema of attribute "browser.name"',
331
445
  );
332
446
  });
333
- });
334
447
 
335
- describe("everyone (*)", () => {
336
448
  it("accepts literal * as conditions", () => {
337
449
  expectConditionsSuccess("*");
338
450
  });
339
- });
340
451
 
341
- describe("conditions as array", () => {
342
- it("accepts array of plain conditions", () => {
452
+ it("accepts arrays of plain conditions", () => {
343
453
  expectConditionsSuccess([
344
454
  { attribute: "userId", operator: "equals", value: "u1" },
345
455
  { attribute: "country", operator: "equals", value: "de" },
346
456
  ]);
347
457
  });
348
458
 
349
- it("rejects array containing invalid condition", () => {
350
- expectConditionsFailure(
351
- [
352
- { attribute: "userId", operator: "equals", value: "u1" },
353
- { attribute: "unknown", operator: "equals", value: "x" },
354
- ],
355
- "Unknown attribute",
356
- );
357
- });
358
- });
359
-
360
- describe("strict: no extra keys on and/or/not", () => {
361
- it("rejects and/or/not with extra key", () => {
459
+ it("rejects and/or/not objects with extra keys", () => {
362
460
  const result = parseConditions({
363
461
  and: [{ attribute: "userId", operator: "equals", value: "u1" }],
364
462
  extraKey: true,
@@ -367,81 +465,25 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
367
465
  });
368
466
  });
369
467
 
370
- describe("operator enum", () => {
371
- it("rejects unknown operator", () => {
372
- const result = parseConditions({
373
- attribute: "userId",
374
- operator: "unknownOp",
375
- value: "x",
376
- });
377
- expect(result.success).toBe(false);
378
- });
379
-
380
- it("accepts all common and numeric operators", () => {
381
- expectConditionsSuccess({ attribute: "userId", operator: "notEquals", value: "x" });
382
- expectConditionsSuccess({ attribute: "userId", operator: "greaterThanOrEquals", value: 0 });
383
- expectConditionsSuccess({ attribute: "userId", operator: "lessThanOrEquals", value: 100 });
384
- });
385
-
386
- it("accepts semver operators with string value", () => {
387
- expectConditionsSuccess({
388
- attribute: "userId",
389
- operator: "semverEquals",
390
- value: "1.0.0",
391
- });
392
- });
393
- });
394
-
395
- describe("errors surface properly: intentional mistakes produce correct path and message", () => {
396
- it("unknown attribute: error path includes attribute, message says Unknown attribute", () => {
468
+ describe("error surfacing", () => {
469
+ it("reports unknown attributes at the attribute path", () => {
397
470
  expectConditionErrorSurfaces(
398
471
  { attribute: "typoAttr", operator: "equals", value: "x" },
399
472
  { pathContains: ["attribute"], messageContains: "Unknown attribute" },
400
473
  );
401
474
  });
402
475
 
403
- it("numeric operator with string value: error path points to value, message says number", () => {
476
+ it("reports schema mismatches at the value path", () => {
404
477
  expectConditionErrorSurfaces(
405
- { attribute: "userId", operator: "greaterThan", value: "10" },
406
- { pathContains: ["value"], messageContains: "number" },
478
+ { attribute: "browser.name", operator: "equals", value: "edge" },
479
+ { pathContains: ["value"], messageContains: "Value does not match the schema" },
407
480
  );
408
481
  });
409
482
 
410
- it("date operator with invalid string: error path points to value, message mentions ISO", () => {
483
+ it("reports array item schema mismatches at the value path", () => {
411
484
  expectConditionErrorSurfaces(
412
- { attribute: "userId", operator: "before", value: "not-a-date" },
413
- { pathContains: ["value"], messageContains: "ISO" },
414
- );
415
- });
416
-
417
- it("exists operator with value set: error path points to value, message says not needed", () => {
418
- expectConditionErrorSurfaces(
419
- { attribute: "userId", operator: "exists", value: "x" },
420
- { pathContains: ["value"], messageContains: "not needed" },
421
- );
422
- });
423
-
424
- it("regexFlags when operator is not matches: error path points to regexFlags", () => {
425
- expectConditionErrorSurfaces(
426
- {
427
- attribute: "userId",
428
- operator: "equals",
429
- value: "u1",
430
- regexFlags: "i",
431
- },
432
- { pathContains: ["regexFlags"], messageContains: "not needed" },
433
- );
434
- });
435
-
436
- it("nested condition with unknown attribute: error path goes into and.*.attribute", () => {
437
- expectConditionErrorSurfaces(
438
- {
439
- and: [
440
- { attribute: "userId", operator: "equals", value: "u1" },
441
- { attribute: "badAttr", operator: "equals", value: "x" },
442
- ],
443
- },
444
- { pathContains: ["attribute"], messageContains: "Unknown attribute" },
485
+ { attribute: "country", operator: "in", value: ["de", "edge"] },
486
+ { pathContains: ["value"], messageContains: "Value at index 1 does not match" },
445
487
  );
446
488
  });
447
489
  });