@featurevisor/core 2.10.0 → 2.12.0

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 (79) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +684 -3
  3. package/coverage/coverage-final.json +4 -0
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  8. package/coverage/lcov-report/builder/index.html +1 -1
  9. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  10. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  11. package/coverage/lcov-report/index.html +25 -10
  12. package/coverage/lcov-report/linter/conditionSchema.ts.html +775 -0
  13. package/coverage/lcov-report/linter/featureSchema.ts.html +4924 -0
  14. package/coverage/lcov-report/linter/index.html +161 -0
  15. package/coverage/lcov-report/linter/schema.ts.html +1471 -0
  16. package/coverage/lcov-report/linter/segmentSchema.ts.html +130 -0
  17. package/coverage/lcov-report/list/index.html +1 -1
  18. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  19. package/coverage/lcov-report/parsers/index.html +1 -1
  20. package/coverage/lcov-report/parsers/json.ts.html +1 -1
  21. package/coverage/lcov-report/parsers/yml.ts.html +1 -1
  22. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  23. package/coverage/lcov-report/tester/index.html +1 -1
  24. package/coverage/lcov.info +1471 -0
  25. package/lib/builder/buildDatafile.js +15 -1
  26. package/lib/builder/buildDatafile.js.map +1 -1
  27. package/lib/config/projectConfig.d.ts +2 -0
  28. package/lib/config/projectConfig.js +3 -1
  29. package/lib/config/projectConfig.js.map +1 -1
  30. package/lib/datasource/datasource.d.ts +6 -1
  31. package/lib/datasource/datasource.js +16 -0
  32. package/lib/datasource/datasource.js.map +1 -1
  33. package/lib/datasource/filesystemAdapter.js +10 -0
  34. package/lib/datasource/filesystemAdapter.js.map +1 -1
  35. package/lib/generate-code/typescript.js +283 -49
  36. package/lib/generate-code/typescript.js.map +1 -1
  37. package/lib/linter/conditionSchema.spec.d.ts +1 -0
  38. package/lib/linter/conditionSchema.spec.js +331 -0
  39. package/lib/linter/conditionSchema.spec.js.map +1 -0
  40. package/lib/linter/featureSchema.d.ts +153 -17
  41. package/lib/linter/featureSchema.js +536 -49
  42. package/lib/linter/featureSchema.js.map +1 -1
  43. package/lib/linter/featureSchema.spec.d.ts +1 -0
  44. package/lib/linter/featureSchema.spec.js +978 -0
  45. package/lib/linter/featureSchema.spec.js.map +1 -0
  46. package/lib/linter/lintProject.js +67 -1
  47. package/lib/linter/lintProject.js.map +1 -1
  48. package/lib/linter/schema.d.ts +42 -0
  49. package/lib/linter/schema.js +417 -0
  50. package/lib/linter/schema.js.map +1 -0
  51. package/lib/linter/schema.spec.d.ts +1 -0
  52. package/lib/linter/schema.spec.js +483 -0
  53. package/lib/linter/schema.spec.js.map +1 -0
  54. package/lib/linter/segmentSchema.spec.d.ts +1 -0
  55. package/lib/linter/segmentSchema.spec.js +231 -0
  56. package/lib/linter/segmentSchema.spec.js.map +1 -0
  57. package/lib/tester/testFeature.js +5 -3
  58. package/lib/tester/testFeature.js.map +1 -1
  59. package/lib/utils/git.js +3 -0
  60. package/lib/utils/git.js.map +1 -1
  61. package/package.json +5 -5
  62. package/src/builder/buildDatafile.ts +17 -1
  63. package/src/config/projectConfig.ts +3 -0
  64. package/src/datasource/datasource.ts +23 -0
  65. package/src/datasource/filesystemAdapter.ts +7 -0
  66. package/src/generate-code/typescript.ts +333 -52
  67. package/src/linter/conditionSchema.spec.ts +446 -0
  68. package/src/linter/featureSchema.spec.ts +1218 -0
  69. package/src/linter/featureSchema.ts +747 -70
  70. package/src/linter/lintProject.ts +84 -0
  71. package/src/linter/schema.spec.ts +617 -0
  72. package/src/linter/schema.ts +462 -0
  73. package/src/linter/segmentSchema.spec.ts +273 -0
  74. package/src/tester/testFeature.ts +5 -3
  75. package/src/utils/git.ts +2 -0
  76. package/lib/linter/propertySchema.d.ts +0 -5
  77. package/lib/linter/propertySchema.js +0 -43
  78. package/lib/linter/propertySchema.js.map +0 -1
  79. package/src/linter/propertySchema.ts +0 -47
@@ -0,0 +1,617 @@
1
+ /**
2
+ * Unit tests for standalone schema validation (schemas/*.yml).
3
+ * Covers getSchemaZodSchema and the refinement helpers: refineEnumMatchesType,
4
+ * refineMinimumMaximum, refineStringLengthPattern, refineArrayItems.
5
+ */
6
+ import { z } from "zod";
7
+
8
+ import {
9
+ refineEnumMatchesType,
10
+ refineMinimumMaximum,
11
+ refineStringLengthPattern,
12
+ refineArrayItems,
13
+ getSchemaZodSchema,
14
+ valueZodSchema,
15
+ propertyTypeEnum,
16
+ } from "./schema";
17
+
18
+ /** Build a refinement context that collects issues for assertion. */
19
+ function createRefinementCtx(): { issues: z.ZodIssue[]; ctx: z.RefinementCtx } {
20
+ const issues: z.ZodIssue[] = [];
21
+ const ctx: z.RefinementCtx = {
22
+ path: [],
23
+ addIssue(issue: z.ZodIssue) {
24
+ issues.push(issue);
25
+ },
26
+ };
27
+ return { issues, ctx };
28
+ }
29
+
30
+ /** Return human-readable message from a Zod issue (custom or default). */
31
+ function issueMessage(issue: z.ZodIssue): string {
32
+ if (issue.code === z.ZodIssueCode.custom && typeof issue.message === "string") {
33
+ return issue.message;
34
+ }
35
+ return issue.message ?? "";
36
+ }
37
+
38
+ describe("schema.ts :: valueZodSchema and propertyTypeEnum", () => {
39
+ describe("valueZodSchema", () => {
40
+ it("accepts boolean, string, number", () => {
41
+ expect(valueZodSchema.safeParse(true).success).toBe(true);
42
+ expect(valueZodSchema.safeParse("x").success).toBe(true);
43
+ expect(valueZodSchema.safeParse(1).success).toBe(true);
44
+ });
45
+
46
+ it("accepts plain objects and arrays of values", () => {
47
+ expect(valueZodSchema.safeParse({ a: 1 }).success).toBe(true);
48
+ expect(valueZodSchema.safeParse([1, "b"]).success).toBe(true);
49
+ });
50
+
51
+ it("rejects null at top level", () => {
52
+ const r = valueZodSchema.safeParse(null);
53
+ expect(r.success).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe("propertyTypeEnum", () => {
58
+ it("accepts allowed types", () => {
59
+ expect(propertyTypeEnum.safeParse("string").success).toBe(true);
60
+ expect(propertyTypeEnum.safeParse("array").success).toBe(true);
61
+ expect(propertyTypeEnum.safeParse("object").success).toBe(true);
62
+ });
63
+
64
+ it("rejects invalid type", () => {
65
+ expect(propertyTypeEnum.safeParse("date").success).toBe(false);
66
+ });
67
+ });
68
+ });
69
+
70
+ describe("schema.ts :: refineEnumMatchesType", () => {
71
+ it("adds no issue when enum values match type string", () => {
72
+ const { issues, ctx } = createRefinementCtx();
73
+ refineEnumMatchesType({ type: "string", enum: ["a", "b"] }, [], ctx);
74
+ expect(issues).toHaveLength(0);
75
+ });
76
+
77
+ it("adds no issue when enum values match type integer", () => {
78
+ const { issues, ctx } = createRefinementCtx();
79
+ refineEnumMatchesType({ type: "integer", enum: [1, 2, 3] }, [], ctx);
80
+ expect(issues).toHaveLength(0);
81
+ });
82
+
83
+ it("adds issue when an enum value does not match type", () => {
84
+ const { issues, ctx } = createRefinementCtx();
85
+ refineEnumMatchesType({ type: "string", enum: ["a", 2, "c"] }, [], ctx);
86
+ expect(issues).toHaveLength(1);
87
+ expect(issueMessage(issues[0])).toContain("index 1");
88
+ expect(issueMessage(issues[0])).toContain('does not match type "string"');
89
+ expect(issues[0].path).toEqual(["enum", 1]);
90
+ });
91
+
92
+ it("adds issue for each enum value that does not match type", () => {
93
+ const { issues, ctx } = createRefinementCtx();
94
+ refineEnumMatchesType({ type: "boolean", enum: [true, "yes", false] }, [], ctx);
95
+ expect(issues.length).toBeGreaterThanOrEqual(1);
96
+ expect(issues.some((i) => issueMessage(i).includes("index 1"))).toBe(true);
97
+ });
98
+
99
+ it("recurses into items and adds issue for enum mismatch in items", () => {
100
+ const { issues, ctx } = createRefinementCtx();
101
+ refineEnumMatchesType(
102
+ {
103
+ type: "array",
104
+ items: { type: "integer", enum: [1, "two", 3] },
105
+ },
106
+ [],
107
+ ctx,
108
+ );
109
+ expect(issues).toHaveLength(1);
110
+ expect(issues[0].path).toEqual(["items", "enum", 1]);
111
+ });
112
+
113
+ it("recurses into properties and adds issue for enum mismatch in property", () => {
114
+ const { issues, ctx } = createRefinementCtx();
115
+ refineEnumMatchesType(
116
+ {
117
+ type: "object",
118
+ properties: {
119
+ level: { type: "integer", enum: [1, 2, 3.5] },
120
+ },
121
+ },
122
+ [],
123
+ ctx,
124
+ );
125
+ expect(issues).toHaveLength(1);
126
+ expect(issues[0].path).toEqual(["properties", "level", "enum", 2]);
127
+ });
128
+
129
+ it("recurses into oneOf branches", () => {
130
+ const { issues, ctx } = createRefinementCtx();
131
+ refineEnumMatchesType(
132
+ {
133
+ oneOf: [
134
+ { type: "string", enum: ["a", "b"] },
135
+ { type: "integer", enum: [1, "two"] },
136
+ ],
137
+ },
138
+ [],
139
+ ctx,
140
+ );
141
+ expect(issues).toHaveLength(1);
142
+ expect(issues[0].path).toEqual(["oneOf", 1, "enum", 1]);
143
+ });
144
+
145
+ it("does nothing when schema is null or not an object", () => {
146
+ const { issues: i1, ctx: c1 } = createRefinementCtx();
147
+ refineEnumMatchesType(null as any, [], c1);
148
+ expect(i1).toHaveLength(0);
149
+ const { issues: i2, ctx: c2 } = createRefinementCtx();
150
+ refineEnumMatchesType({ type: "string" }, [], c2);
151
+ expect(i2).toHaveLength(0);
152
+ });
153
+ });
154
+
155
+ describe("schema.ts :: refineMinimumMaximum", () => {
156
+ it("adds no issue when minimum <= maximum", () => {
157
+ const { issues, ctx } = createRefinementCtx();
158
+ refineMinimumMaximum({ type: "integer", minimum: 0, maximum: 10 }, [], ctx);
159
+ expect(issues).toHaveLength(0);
160
+ });
161
+
162
+ it("adds issue when minimum > maximum for integer", () => {
163
+ const { issues, ctx } = createRefinementCtx();
164
+ refineMinimumMaximum({ type: "integer", minimum: 10, maximum: 5 }, [], ctx);
165
+ expect(issues).toHaveLength(1);
166
+ expect(issueMessage(issues[0])).toContain("minimum");
167
+ expect(issueMessage(issues[0])).toContain("maximum");
168
+ expect(issues[0].path).toEqual(["minimum"]);
169
+ });
170
+
171
+ it("adds issue when minimum > maximum for double", () => {
172
+ const { issues, ctx } = createRefinementCtx();
173
+ refineMinimumMaximum({ type: "double", minimum: 1.5, maximum: 1.0 }, [], ctx);
174
+ expect(issues).toHaveLength(1);
175
+ });
176
+
177
+ it("adds issue when const is less than minimum", () => {
178
+ const { issues, ctx } = createRefinementCtx();
179
+ refineMinimumMaximum({ type: "integer", minimum: 10, const: 5 }, [], ctx);
180
+ expect(issues).toHaveLength(1);
181
+ expect(issueMessage(issues[0])).toContain("const");
182
+ expect(issueMessage(issues[0])).toContain("minimum");
183
+ });
184
+
185
+ it("adds issue when const is greater than maximum", () => {
186
+ const { issues, ctx } = createRefinementCtx();
187
+ refineMinimumMaximum({ type: "integer", maximum: 10, const: 15 }, [], ctx);
188
+ expect(issues).toHaveLength(1);
189
+ });
190
+
191
+ it("adds issue when an enum value is out of range", () => {
192
+ const { issues, ctx } = createRefinementCtx();
193
+ refineMinimumMaximum({ type: "integer", minimum: 1, maximum: 5, enum: [1, 3, 10] }, [], ctx);
194
+ expect(issues).toHaveLength(1);
195
+ expect(issueMessage(issues[0])).toContain("index 2");
196
+ expect(issueMessage(issues[0])).toContain("maximum");
197
+ });
198
+
199
+ it("does nothing for non-numeric types", () => {
200
+ const { issues, ctx } = createRefinementCtx();
201
+ refineMinimumMaximum({ type: "string", minimum: 0, maximum: 10 }, [], ctx);
202
+ expect(issues).toHaveLength(0);
203
+ });
204
+
205
+ it("recurses into items", () => {
206
+ const { issues, ctx } = createRefinementCtx();
207
+ refineMinimumMaximum(
208
+ {
209
+ type: "array",
210
+ items: { type: "integer", minimum: 20, maximum: 10 },
211
+ },
212
+ [],
213
+ ctx,
214
+ );
215
+ expect(issues).toHaveLength(1);
216
+ expect(issues[0].path).toEqual(["items", "minimum"]);
217
+ });
218
+ });
219
+
220
+ describe("schema.ts :: refineStringLengthPattern", () => {
221
+ it("adds no issue when minLength <= maxLength", () => {
222
+ const { issues, ctx } = createRefinementCtx();
223
+ refineStringLengthPattern({ type: "string", minLength: 1, maxLength: 10 }, [], ctx);
224
+ expect(issues).toHaveLength(0);
225
+ });
226
+
227
+ it("adds issue when minLength > maxLength", () => {
228
+ const { issues, ctx } = createRefinementCtx();
229
+ refineStringLengthPattern({ type: "string", minLength: 10, maxLength: 5 }, [], ctx);
230
+ expect(issues).toHaveLength(1);
231
+ expect(issueMessage(issues[0])).toContain("minLength");
232
+ expect(issues[0].path).toEqual(["minLength"]);
233
+ });
234
+
235
+ it("adds issue when pattern is invalid regex", () => {
236
+ const { issues, ctx } = createRefinementCtx();
237
+ refineStringLengthPattern({ type: "string", pattern: "[" }, [], ctx);
238
+ expect(issues).toHaveLength(1);
239
+ expect(issueMessage(issues[0])).toContain("pattern");
240
+ expect(issueMessage(issues[0])).toContain("invalid");
241
+ });
242
+
243
+ it("adds no issue when pattern is valid", () => {
244
+ const { issues, ctx } = createRefinementCtx();
245
+ refineStringLengthPattern({ type: "string", pattern: "^[a-z]+$" }, [], ctx);
246
+ expect(issues).toHaveLength(0);
247
+ });
248
+
249
+ it("adds issue when const string is shorter than minLength", () => {
250
+ const { issues, ctx } = createRefinementCtx();
251
+ refineStringLengthPattern({ type: "string", minLength: 5, const: "ab" }, [], ctx);
252
+ expect(issues).toHaveLength(1);
253
+ expect(issueMessage(issues[0])).toContain("const");
254
+ });
255
+
256
+ it("adds issue when const string is longer than maxLength", () => {
257
+ const { issues, ctx } = createRefinementCtx();
258
+ refineStringLengthPattern({ type: "string", maxLength: 2, const: "hello" }, [], ctx);
259
+ expect(issues).toHaveLength(1);
260
+ });
261
+
262
+ it("adds issue when const string does not match pattern", () => {
263
+ const { issues, ctx } = createRefinementCtx();
264
+ refineStringLengthPattern({ type: "string", pattern: "^[0-9]+$", const: "abc" }, [], ctx);
265
+ expect(issues).toHaveLength(1);
266
+ expect(issueMessage(issues[0])).toContain("pattern");
267
+ });
268
+
269
+ it("adds issue when an enum string value violates length or pattern", () => {
270
+ const { issues, ctx } = createRefinementCtx();
271
+ refineStringLengthPattern({ type: "string", maxLength: 2, enum: ["a", "abc", "b"] }, [], ctx);
272
+ expect(issues).toHaveLength(1);
273
+ expect(issueMessage(issues[0])).toContain("index 1");
274
+ });
275
+
276
+ it("recurses into items and properties", () => {
277
+ const { issues, ctx } = createRefinementCtx();
278
+ refineStringLengthPattern(
279
+ {
280
+ type: "object",
281
+ properties: {
282
+ code: { type: "string", minLength: 10, maxLength: 5 },
283
+ },
284
+ },
285
+ [],
286
+ ctx,
287
+ );
288
+ expect(issues).toHaveLength(1);
289
+ expect(issues[0].path).toEqual(["properties", "code", "minLength"]);
290
+ });
291
+ });
292
+
293
+ describe("schema.ts :: refineArrayItems", () => {
294
+ it("adds no issue when minItems <= maxItems", () => {
295
+ const { issues, ctx } = createRefinementCtx();
296
+ refineArrayItems({ type: "array", minItems: 0, maxItems: 10 }, [], ctx);
297
+ expect(issues).toHaveLength(0);
298
+ });
299
+
300
+ it("adds issue when minItems > maxItems", () => {
301
+ const { issues, ctx } = createRefinementCtx();
302
+ refineArrayItems({ type: "array", minItems: 10, maxItems: 5 }, [], ctx);
303
+ expect(issues).toHaveLength(1);
304
+ expect(issueMessage(issues[0])).toContain("minItems");
305
+ expect(issues[0].path).toEqual(["minItems"]);
306
+ });
307
+
308
+ it("adds issue when const array length is less than minItems", () => {
309
+ const { issues, ctx } = createRefinementCtx();
310
+ refineArrayItems({ type: "array", minItems: 3, const: [1, 2] }, [], ctx);
311
+ expect(issues).toHaveLength(1);
312
+ expect(issueMessage(issues[0])).toContain("minItems");
313
+ });
314
+
315
+ it("adds issue when const array length is greater than maxItems", () => {
316
+ const { issues, ctx } = createRefinementCtx();
317
+ refineArrayItems({ type: "array", maxItems: 2, const: [1, 2, 3] }, [], ctx);
318
+ expect(issues).toHaveLength(1);
319
+ });
320
+
321
+ it("adds issue when uniqueItems is true and const array has duplicates", () => {
322
+ const { issues, ctx } = createRefinementCtx();
323
+ refineArrayItems({ type: "array", uniqueItems: true, const: [1, 2, 1] }, [], ctx);
324
+ expect(issues).toHaveLength(1);
325
+ expect(issueMessage(issues[0])).toContain("duplicate");
326
+ });
327
+
328
+ it("adds issue when uniqueItems is true and enum array value has duplicates", () => {
329
+ const { issues, ctx } = createRefinementCtx();
330
+ refineArrayItems(
331
+ {
332
+ type: "array",
333
+ uniqueItems: true,
334
+ enum: [
335
+ [1, 2],
336
+ [3, 3],
337
+ [4, 5],
338
+ ],
339
+ },
340
+ [],
341
+ ctx,
342
+ );
343
+ expect(issues).toHaveLength(1);
344
+ expect(issues[0].path).toEqual(["enum", 1]);
345
+ });
346
+
347
+ it("recurses into items", () => {
348
+ const { issues, ctx } = createRefinementCtx();
349
+ refineArrayItems(
350
+ {
351
+ type: "array",
352
+ items: { type: "array", minItems: 5, maxItems: 2 },
353
+ },
354
+ [],
355
+ ctx,
356
+ );
357
+ expect(issues).toHaveLength(1);
358
+ expect(issues[0].path).toEqual(["items", "minItems"]);
359
+ });
360
+ });
361
+
362
+ describe("schema.ts :: getSchemaZodSchema", () => {
363
+ it("accepts a minimal valid schema with type and items for array", () => {
364
+ const Schema = getSchemaZodSchema([]);
365
+ const result = Schema.safeParse({
366
+ type: "array",
367
+ items: { type: "string" },
368
+ });
369
+ expect(result.success).toBe(true);
370
+ });
371
+
372
+ it("accepts a schema with description only (no type)", () => {
373
+ const Schema = getSchemaZodSchema([]);
374
+ const result = Schema.safeParse({ description: "A schema" });
375
+ expect(result.success).toBe(true);
376
+ });
377
+
378
+ it("rejects unknown schema reference when key not in schemaKeys", () => {
379
+ const Schema = getSchemaZodSchema(["link", "color"]);
380
+ const result = Schema.safeParse({ schema: "nonexistent" });
381
+ expect(result.success).toBe(false);
382
+ const err = !result.success ? (result as z.SafeParseError<unknown>).error : null;
383
+ expect(err?.issues.some((i) => i.message === 'Unknown schema "nonexistent"')).toBe(true);
384
+ });
385
+
386
+ it("accepts schema reference when key is in schemaKeys", () => {
387
+ const Schema = getSchemaZodSchema(["link"]);
388
+ const result = Schema.safeParse({ schema: "link" });
389
+ expect(result.success).toBe(true);
390
+ });
391
+
392
+ it("rejects oneOf with only one branch (requires min 2)", () => {
393
+ const Schema = getSchemaZodSchema([]);
394
+ const result = Schema.safeParse({
395
+ oneOf: [{ type: "string" }],
396
+ });
397
+ expect(result.success).toBe(false);
398
+ const err = !result.success ? (result as z.SafeParseError<unknown>).error : null;
399
+ const msg =
400
+ err?.issues.map((i) => (typeof i.message === "string" ? i.message : "")).join(" ") ?? "";
401
+ expect(msg).toMatch(/array|length|minimum|oneOf|element/i);
402
+ });
403
+
404
+ it("accepts oneOf with at least two branches", () => {
405
+ const Schema = getSchemaZodSchema(["link"]);
406
+ const result = Schema.safeParse({
407
+ oneOf: [{ type: "string" }, { schema: "link" }],
408
+ });
409
+ expect(result.success).toBe(true);
410
+ });
411
+
412
+ it("rejects type array without items", () => {
413
+ const Schema = getSchemaZodSchema([]);
414
+ const result = Schema.safeParse({ type: "array" });
415
+ expect(result.success).toBe(false);
416
+ const err = !result.success ? (result as z.SafeParseError<unknown>).error : null;
417
+ expect(
418
+ err?.issues.some(
419
+ (i) =>
420
+ typeof i.message === "string" &&
421
+ i.message.includes("array") &&
422
+ i.message.includes("items"),
423
+ ),
424
+ ).toBe(true);
425
+ });
426
+
427
+ it("rejects extra keys (strict)", () => {
428
+ const Schema = getSchemaZodSchema([]);
429
+ const result = Schema.safeParse({
430
+ type: "string",
431
+ unknownKey: true,
432
+ });
433
+ expect(result.success).toBe(false);
434
+ });
435
+
436
+ it("rejects enum value that does not match type (via refinement)", () => {
437
+ const Schema = getSchemaZodSchema([]);
438
+ const result = Schema.safeParse({
439
+ type: "string",
440
+ enum: ["a", 42, "c"],
441
+ });
442
+ expect(result.success).toBe(false);
443
+ const err = !result.success ? (result as z.SafeParseError<unknown>).error : null;
444
+ expect(
445
+ err?.issues.some(
446
+ (i) => typeof i.message === "string" && i.message.includes("does not match type"),
447
+ ),
448
+ ).toBe(true);
449
+ });
450
+
451
+ it("rejects minimum > maximum (via refinement)", () => {
452
+ const Schema = getSchemaZodSchema([]);
453
+ const result = Schema.safeParse({
454
+ type: "integer",
455
+ minimum: 10,
456
+ maximum: 5,
457
+ });
458
+ expect(result.success).toBe(false);
459
+ });
460
+
461
+ it("rejects invalid pattern (via refinement)", () => {
462
+ const Schema = getSchemaZodSchema([]);
463
+ const result = Schema.safeParse({
464
+ type: "string",
465
+ pattern: "[unclosed",
466
+ });
467
+ expect(result.success).toBe(false);
468
+ });
469
+
470
+ it("rejects minItems > maxItems (via refinement)", () => {
471
+ const Schema = getSchemaZodSchema([]);
472
+ const result = Schema.safeParse({
473
+ type: "array",
474
+ items: { type: "string" },
475
+ minItems: 10,
476
+ maxItems: 2,
477
+ });
478
+ expect(result.success).toBe(false);
479
+ });
480
+
481
+ it("accepts object schema with properties and required", () => {
482
+ const Schema = getSchemaZodSchema([]);
483
+ const result = Schema.safeParse({
484
+ type: "object",
485
+ required: ["id"],
486
+ properties: {
487
+ id: { type: "string" },
488
+ name: { type: "string" },
489
+ },
490
+ });
491
+ expect(result.success).toBe(true);
492
+ });
493
+
494
+ it("accepts array schema with items that reference another schema", () => {
495
+ const Schema = getSchemaZodSchema(["link"]);
496
+ const result = Schema.safeParse({
497
+ type: "array",
498
+ items: { schema: "link" },
499
+ });
500
+ expect(result.success).toBe(true);
501
+ });
502
+
503
+ it("accepts const schema with valid value", () => {
504
+ const Schema = getSchemaZodSchema([]);
505
+ const result = Schema.safeParse({
506
+ type: "string",
507
+ const: "draft",
508
+ });
509
+ expect(result.success).toBe(true);
510
+ });
511
+
512
+ it("accepts boolean type", () => {
513
+ const Schema = getSchemaZodSchema([]);
514
+ const result = Schema.safeParse({ type: "boolean" });
515
+ expect(result.success).toBe(true);
516
+ });
517
+
518
+ it("accepts double type with minimum and maximum", () => {
519
+ const Schema = getSchemaZodSchema([]);
520
+ const result = Schema.safeParse({
521
+ type: "double",
522
+ minimum: 0,
523
+ maximum: 1,
524
+ });
525
+ expect(result.success).toBe(true);
526
+ });
527
+
528
+ it("accepts object schema with property that references another schema (e.g. productSummary)", () => {
529
+ const Schema = getSchemaZodSchema(["money", "image"]);
530
+ const result = Schema.safeParse({
531
+ type: "object",
532
+ description: "Product summary",
533
+ properties: {
534
+ id: { type: "string" },
535
+ name: { type: "string" },
536
+ price: { schema: "money" },
537
+ image: { schema: "image" },
538
+ },
539
+ required: ["id", "name", "price"],
540
+ });
541
+ expect(result.success).toBe(true);
542
+ });
543
+
544
+ describe("errors surface properly: intentional mistakes produce correct path and message", () => {
545
+ function expectSchemaErrorSurfaces(
546
+ schemaKeys: string[],
547
+ invalidSchema: unknown,
548
+ opts: { pathContains: string[]; messageContains: string },
549
+ ): void {
550
+ const Schema = getSchemaZodSchema(schemaKeys);
551
+ const result = Schema.safeParse(invalidSchema);
552
+ expect(result.success).toBe(false);
553
+ const err = !result.success ? (result as z.SafeParseError<unknown>).error : null;
554
+ expect(err).not.toBeNull();
555
+ const messages = (err?.issues ?? [])
556
+ .map((i) => (typeof i.message === "string" ? i.message : ""))
557
+ .join(" ");
558
+ expect(messages).toContain(opts.messageContains);
559
+ const pathStrings = (err?.issues ?? []).map((i) => i.path.join("."));
560
+ const hasMatchingPath = pathStrings.some((p) =>
561
+ opts.pathContains.every((seg) => p.includes(seg)),
562
+ );
563
+ expect(hasMatchingPath).toBe(true);
564
+ }
565
+
566
+ it("unknown schema ref: error path includes schema, message says Unknown schema", () => {
567
+ expectSchemaErrorSurfaces(
568
+ ["link"],
569
+ { schema: "nonexistent" },
570
+ { pathContains: ["schema"], messageContains: "Unknown schema" },
571
+ );
572
+ });
573
+
574
+ it("array without items: error path points to items, message mentions array and items", () => {
575
+ expectSchemaErrorSurfaces(
576
+ [],
577
+ { type: "array" },
578
+ { pathContains: ["items"], messageContains: "array" },
579
+ );
580
+ });
581
+
582
+ it("minimum > maximum: error path points to minimum, message mentions minimum and maximum", () => {
583
+ expectSchemaErrorSurfaces(
584
+ [],
585
+ { type: "integer", minimum: 20, maximum: 10 },
586
+ { pathContains: ["minimum"], messageContains: "minimum" },
587
+ );
588
+ });
589
+
590
+ it("invalid pattern: error path points to pattern, message mentions pattern", () => {
591
+ expectSchemaErrorSurfaces(
592
+ [],
593
+ { type: "string", pattern: "[" },
594
+ { pathContains: ["pattern"], messageContains: "pattern" },
595
+ );
596
+ });
597
+
598
+ it("enum value does not match type: error path points to enum index, message says match type", () => {
599
+ expectSchemaErrorSurfaces(
600
+ [],
601
+ { type: "string", enum: ["a", 42, "c"] },
602
+ { pathContains: ["enum"], messageContains: "match type" },
603
+ );
604
+ });
605
+
606
+ it("extra key: parse fails and message mentions unrecognized key", () => {
607
+ const Schema = getSchemaZodSchema([]);
608
+ const result = Schema.safeParse({ type: "string", invalidKey: true });
609
+ expect(result.success).toBe(false);
610
+ const err = !result.success ? (result as z.SafeParseError<unknown>).error : null;
611
+ const messages = (err?.issues ?? [])
612
+ .map((i) => (typeof i.message === "string" ? i.message : ""))
613
+ .join(" ");
614
+ expect(messages).toMatch(/unrecognized|invalidKey/i);
615
+ });
616
+ });
617
+ });