com.wallstop-studios.dxmessaging 2.1.5 → 2.1.6

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 (161) hide show
  1. package/.artifacts/SourceGenerators.Tests/obj/Debug/net9.0/WallstopStudios.DxMessaging.SourceGenerators.Tests.AssemblyInfo.cs +1 -1
  2. package/.cspell.json +4 -1
  3. package/.github/workflows/actionlint.yml +11 -1
  4. package/.github/workflows/csharpier-autofix.yml +34 -3
  5. package/.github/workflows/dotnet-tests.yml +13 -0
  6. package/.github/workflows/format-on-demand.yml +38 -44
  7. package/.github/workflows/json-format-check.yml +24 -0
  8. package/.github/workflows/lint-doc-links.yml +13 -0
  9. package/.github/workflows/markdown-json.yml +21 -4
  10. package/.github/workflows/markdown-link-text-check.yml +10 -0
  11. package/.github/workflows/markdown-link-validity.yml +10 -0
  12. package/.github/workflows/markdownlint.yml +7 -5
  13. package/.github/workflows/prettier-autofix.yml +67 -11
  14. package/.github/workflows/release-drafter.yml +2 -2
  15. package/.github/workflows/sync-wiki.yml +3 -3
  16. package/.github/workflows/yaml-format-lint.yml +26 -0
  17. package/.llm/context.md +113 -3
  18. package/.llm/skills/documentation/changelog-management.md +38 -0
  19. package/.llm/skills/documentation/documentation-style-guide.md +18 -0
  20. package/.llm/skills/documentation/documentation-update-workflow.md +2 -0
  21. package/.llm/skills/documentation/documentation-updates.md +2 -0
  22. package/.llm/skills/documentation/markdown-compatibility.md +476 -0
  23. package/.llm/skills/documentation/mermaid-theming.md +326 -0
  24. package/.llm/skills/documentation/mkdocs-navigation.md +290 -0
  25. package/.llm/skills/github-actions/git-renormalize-patterns.md +231 -0
  26. package/.llm/skills/github-actions/workflow-consistency.md +346 -0
  27. package/.llm/skills/index.md +53 -27
  28. package/.llm/skills/scripting/javascript-code-quality.md +417 -0
  29. package/.llm/skills/scripting/regex-documentation.md +461 -0
  30. package/.llm/skills/scripting/shell-best-practices.md +55 -4
  31. package/.llm/skills/scripting/validation-patterns.md +418 -0
  32. package/.llm/skills/specification.md +4 -1
  33. package/.llm/skills/testing/test-code-quality.md +243 -0
  34. package/.llm/skills/testing/test-production-code.md +348 -0
  35. package/CHANGELOG.md +11 -0
  36. package/README.md +0 -11
  37. package/Tests/Runtime/Benchmarks/WallstopStudios.DxMessaging.Tests.Runtime.Benchmarks.asmdef +1 -6
  38. package/Tests/Runtime/Integrations/Reflex/WallstopStudios.DxMessaging.Tests.Runtime.Reflex.asmdef +1 -1
  39. package/Tests/Runtime/Integrations/VContainer/WallstopStudios.DxMessaging.Tests.Runtime.VContainer.asmdef +1 -1
  40. package/Tests/Runtime/Integrations/Zenject/WallstopStudios.DxMessaging.Tests.Runtime.Zenject.asmdef +1 -1
  41. package/coverage/clover.xml +216 -3
  42. package/coverage/clover.xml.meta +7 -7
  43. package/coverage/coverage-final.json +2 -1
  44. package/coverage/coverage-final.json.meta +7 -7
  45. package/coverage/lcov-report/base.css.meta +1 -1
  46. package/coverage/lcov-report/block-navigation.js.meta +1 -1
  47. package/coverage/lcov-report/favicon.png.meta +1 -1
  48. package/coverage/lcov-report/index.html +25 -10
  49. package/coverage/lcov-report/index.html.meta +7 -7
  50. package/coverage/lcov-report/prettify.css.meta +1 -1
  51. package/coverage/lcov-report/prettify.js.meta +1 -1
  52. package/coverage/lcov-report/sort-arrow-sprite.png.meta +1 -1
  53. package/coverage/lcov-report/sorter.js.meta +1 -1
  54. package/coverage/lcov-report/transform-docs-to-wiki.js.html +1 -1
  55. package/coverage/lcov-report/transform-docs-to-wiki.js.html.meta +7 -7
  56. package/coverage/lcov-report/vendor.meta +1 -1
  57. package/coverage/lcov-report.meta +8 -8
  58. package/coverage/lcov.info +365 -0
  59. package/coverage/lcov.info.meta +7 -7
  60. package/docs/architecture/design-and-architecture.md +0 -1
  61. package/docs/concepts/index.md +37 -0
  62. package/docs/concepts/index.md.meta +7 -0
  63. package/docs/concepts/interceptors-and-ordering.md +0 -2
  64. package/docs/concepts/mental-model.md +390 -0
  65. package/docs/concepts/mental-model.md.meta +7 -0
  66. package/docs/concepts/message-types.md +0 -1
  67. package/docs/getting-started/getting-started.md +1 -0
  68. package/docs/getting-started/index.md +6 -5
  69. package/docs/getting-started/overview.md +1 -0
  70. package/docs/getting-started/quick-start.md +2 -1
  71. package/docs/getting-started/visual-guide.md +4 -10
  72. package/docs/hooks.py +10 -1
  73. package/docs/images/DxMessaging-banner.svg +1 -1
  74. package/docs/index.md +7 -7
  75. package/docs/javascripts/mermaid-config.js +44 -4
  76. package/docs/reference/helpers.md +130 -154
  77. package/docs/reference/quick-reference.md +5 -1
  78. package/docs/reference/reference.md +124 -130
  79. package/mkdocs.yml +2 -0
  80. package/package.json +1 -1
  81. package/scripts/__tests__/generate-skills-index.test.js +397 -0
  82. package/scripts/__tests__/generate-skills-index.test.js.meta +7 -0
  83. package/scripts/__tests__/mermaid-config.test.js +467 -0
  84. package/scripts/__tests__/mermaid-config.test.js.meta +7 -0
  85. package/scripts/__tests__/validate-skills-optional-fields.test.js +1474 -0
  86. package/scripts/__tests__/validate-skills-optional-fields.test.js.meta +7 -0
  87. package/scripts/__tests__/validate-skills-required-fields.test.js +188 -0
  88. package/scripts/__tests__/validate-skills-required-fields.test.js.meta +7 -0
  89. package/scripts/__tests__/validate-skills-tags.test.js +353 -0
  90. package/scripts/__tests__/validate-skills-tags.test.js.meta +7 -0
  91. package/scripts/__tests__/validate-workflows.test.js +188 -0
  92. package/scripts/__tests__/validate-workflows.test.js.meta +7 -0
  93. package/scripts/generate-skills-index.js +88 -3
  94. package/scripts/validate-skills.js +230 -30
  95. package/scripts/validate-workflows.js +272 -0
  96. package/scripts/validate-workflows.js.meta +7 -0
  97. package/site/404.html +1 -1
  98. package/site/advanced/emit-shorthands/index.html +2 -2
  99. package/site/advanced/message-bus-providers/index.html +2 -2
  100. package/site/advanced/registration-builders/index.html +2 -2
  101. package/site/advanced/runtime-configuration/index.html +2 -2
  102. package/site/advanced/string-messages/index.html +2 -2
  103. package/site/advanced.meta +1 -1
  104. package/site/architecture/comparisons/index.html +2 -2
  105. package/site/architecture/design-and-architecture/index.html +2 -2
  106. package/site/architecture/performance/index.html +1 -1
  107. package/site/architecture.meta +1 -1
  108. package/site/concepts/index.html +1 -0
  109. package/site/concepts/index.html.meta +7 -0
  110. package/site/concepts/interceptors-and-ordering/index.html +4 -4
  111. package/site/concepts/listening-patterns/index.html +2 -2
  112. package/site/concepts/mental-model/index.html +146 -0
  113. package/site/concepts/mental-model/index.html.meta +7 -0
  114. package/site/concepts/mental-model.meta +8 -0
  115. package/site/concepts/message-types/index.html +2 -2
  116. package/site/concepts/targeting-and-context/index.html +2 -2
  117. package/site/concepts.meta +1 -1
  118. package/site/examples/end-to-end/index.html +2 -2
  119. package/site/examples/end-to-end-scene-transitions/index.html +2 -2
  120. package/site/examples.meta +1 -1
  121. package/site/getting-started/getting-started/index.html +3 -3
  122. package/site/getting-started/index.html +4 -4
  123. package/site/getting-started/install/index.html +3 -3
  124. package/site/getting-started/overview/index.html +2 -2
  125. package/site/getting-started/quick-start/index.html +2 -2
  126. package/site/getting-started/visual-guide/index.html +11 -11
  127. package/site/getting-started.meta +1 -1
  128. package/site/guides/advanced/index.html +2 -2
  129. package/site/guides/diagnostics/index.html +2 -2
  130. package/site/guides/migration-guide/index.html +2 -2
  131. package/site/guides/patterns/index.html +2 -2
  132. package/site/guides/testing/index.html +2 -2
  133. package/site/guides/unity-integration/index.html +2 -2
  134. package/site/guides.meta +1 -1
  135. package/site/hooks.py.meta +1 -1
  136. package/site/images/DxMessaging-banner.svg +119 -0
  137. package/site/images/DxMessaging-banner.svg.meta +7 -0
  138. package/site/images.meta +8 -0
  139. package/site/index.html +2 -2
  140. package/site/integrations/index.html +2 -2
  141. package/site/integrations/reflex/index.html +2 -2
  142. package/site/integrations/vcontainer/index.html +2 -2
  143. package/site/integrations/zenject/index.html +2 -2
  144. package/site/integrations.meta +1 -1
  145. package/site/javascripts/csharp-highlight.js.meta +7 -7
  146. package/site/javascripts/mermaid-config.js +4 -1
  147. package/site/javascripts/mermaid-config.js.meta +1 -1
  148. package/site/javascripts.meta +1 -1
  149. package/site/reference/compatibility/index.html +1 -1
  150. package/site/reference/faq/index.html +1 -1
  151. package/site/reference/glossary/index.html +2 -2
  152. package/site/reference/helpers/index.html +15 -15
  153. package/site/reference/quick-reference/index.html +3 -3
  154. package/site/reference/reference/index.html +37 -37
  155. package/site/reference/troubleshooting/index.html +1 -1
  156. package/site/reference.meta +1 -1
  157. package/site/search/search_index.json +1 -1
  158. package/site/sitemap.xml +46 -38
  159. package/site/sitemap.xml.gz +0 -0
  160. package/site/stylesheets/extra.css.meta +1 -1
  161. package/site/stylesheets.meta +1 -1
@@ -0,0 +1,1474 @@
1
+ /**
2
+ * @fileoverview Tests for validate-skills.js optional field validation logic.
3
+ *
4
+ * These tests validate the missing optional field detection in skill files:
5
+ * - complexity.level - affects Complexity column in skills index
6
+ * - impact.performance.rating - affects Performance column in skills index
7
+ *
8
+ * The validation logic detects missing optional fields that cause '?'
9
+ * placeholders in the generated skills index.
10
+ */
11
+
12
+ "use strict";
13
+
14
+ const {
15
+ validateComplexityLevel,
16
+ validatePerformanceRating,
17
+ isValidImpactObject,
18
+ } = require('../validate-skills.js');
19
+
20
+ describe("validate-skills optional field validation", () => {
21
+ const testPath = "testing/sample-skill.md";
22
+
23
+ describe("complexity.level validation", () => {
24
+ describe("missing complexity field", () => {
25
+ test("should warn when complexity is undefined", () => {
26
+ const frontmatter = {
27
+ title: "Sample Skill",
28
+ id: "sample-skill",
29
+ // complexity is undefined
30
+ };
31
+
32
+ const warnings = validateComplexityLevel(frontmatter, testPath);
33
+
34
+ expect(warnings).toHaveLength(1);
35
+ expect(warnings[0].field).toBe("complexity.level");
36
+ expect(warnings[0].message).toContain("Missing 'complexity.level'");
37
+ expect(warnings[0].message).toContain("Complexity column");
38
+ });
39
+
40
+ test("should warn when complexity is null", () => {
41
+ const frontmatter = {
42
+ title: "Sample Skill",
43
+ id: "sample-skill",
44
+ complexity: null,
45
+ };
46
+
47
+ const warnings = validateComplexityLevel(frontmatter, testPath);
48
+
49
+ expect(warnings).toHaveLength(1);
50
+ expect(warnings[0].field).toBe("complexity.level");
51
+ expect(warnings[0].message).toContain("Missing 'complexity.level'");
52
+ expect(warnings[0].message).toContain("Complexity column");
53
+ });
54
+
55
+ test("should warn when complexity is an empty object", () => {
56
+ const frontmatter = {
57
+ title: "Sample Skill",
58
+ id: "sample-skill",
59
+ complexity: {},
60
+ };
61
+
62
+ const warnings = validateComplexityLevel(frontmatter, testPath);
63
+
64
+ expect(warnings).toHaveLength(1);
65
+ expect(warnings[0].field).toBe("complexity.level");
66
+ expect(warnings[0].message).toContain("Missing 'complexity.level'");
67
+ });
68
+
69
+ test("should warn when complexity.level is undefined", () => {
70
+ const frontmatter = {
71
+ title: "Sample Skill",
72
+ id: "sample-skill",
73
+ complexity: {
74
+ // level is undefined
75
+ description: "Some description",
76
+ },
77
+ };
78
+
79
+ const warnings = validateComplexityLevel(frontmatter, testPath);
80
+
81
+ expect(warnings).toHaveLength(1);
82
+ expect(warnings[0].field).toBe("complexity.level");
83
+ expect(warnings[0].message).toContain("Missing 'complexity.level'");
84
+ });
85
+
86
+ test("should warn when complexity.level is null", () => {
87
+ const frontmatter = {
88
+ title: "Sample Skill",
89
+ id: "sample-skill",
90
+ complexity: {
91
+ level: null,
92
+ },
93
+ };
94
+
95
+ const warnings = validateComplexityLevel(frontmatter, testPath);
96
+
97
+ expect(warnings).toHaveLength(1);
98
+ expect(warnings[0].field).toBe("complexity.level");
99
+ expect(warnings[0].message).toContain("Missing 'complexity.level'");
100
+ });
101
+
102
+ test("should warn when complexity.level is an empty string", () => {
103
+ const frontmatter = {
104
+ title: "Sample Skill",
105
+ id: "sample-skill",
106
+ complexity: {
107
+ level: "",
108
+ },
109
+ };
110
+
111
+ const warnings = validateComplexityLevel(frontmatter, testPath);
112
+
113
+ expect(warnings).toHaveLength(1);
114
+ expect(warnings[0].field).toBe("complexity.level");
115
+ expect(warnings[0].message).toContain("Empty 'complexity.level'");
116
+ });
117
+ });
118
+
119
+ describe("valid complexity.level", () => {
120
+ test("should not warn when complexity.level is a valid string", () => {
121
+ const frontmatter = {
122
+ title: "Sample Skill",
123
+ id: "sample-skill",
124
+ complexity: {
125
+ level: "intermediate",
126
+ },
127
+ };
128
+
129
+ const warnings = validateComplexityLevel(frontmatter, testPath);
130
+
131
+ expect(warnings).toHaveLength(0);
132
+ });
133
+
134
+ test("should not warn when complexity.level is a number", () => {
135
+ const frontmatter = {
136
+ title: "Sample Skill",
137
+ id: "sample-skill",
138
+ complexity: {
139
+ level: 3,
140
+ },
141
+ };
142
+
143
+ const warnings = validateComplexityLevel(frontmatter, testPath);
144
+
145
+ expect(warnings).toHaveLength(0);
146
+ });
147
+ });
148
+
149
+ describe("warning message format", () => {
150
+ test("should include correct column name in warning message", () => {
151
+ const frontmatter = {
152
+ title: "Sample Skill",
153
+ id: "sample-skill",
154
+ };
155
+
156
+ const warnings = validateComplexityLevel(frontmatter, testPath);
157
+
158
+ expect(warnings[0].message).toBe(
159
+ "Missing 'complexity.level' - will show '?' in Complexity column of skills index"
160
+ );
161
+ });
162
+
163
+ test("should include correct field name in warning", () => {
164
+ const frontmatter = {
165
+ title: "Sample Skill",
166
+ id: "sample-skill",
167
+ };
168
+
169
+ const warnings = validateComplexityLevel(frontmatter, testPath);
170
+
171
+ expect(warnings[0].field).toBe("complexity.level");
172
+ });
173
+ });
174
+ });
175
+
176
+ describe("impact.performance.rating validation", () => {
177
+ describe("missing impact field", () => {
178
+ test("should warn when impact is undefined", () => {
179
+ const frontmatter = {
180
+ title: "Sample Skill",
181
+ id: "sample-skill",
182
+ // impact is undefined
183
+ };
184
+
185
+ const warnings = validatePerformanceRating(frontmatter, testPath);
186
+
187
+ expect(warnings).toHaveLength(1);
188
+ expect(warnings[0].field).toBe("impact.performance.rating");
189
+ expect(warnings[0].message).toContain("Missing 'impact.performance.rating'");
190
+ expect(warnings[0].message).toContain("Performance column");
191
+ });
192
+
193
+ test("should warn when impact is null", () => {
194
+ const frontmatter = {
195
+ title: "Sample Skill",
196
+ id: "sample-skill",
197
+ impact: null,
198
+ };
199
+
200
+ const warnings = validatePerformanceRating(frontmatter, testPath);
201
+
202
+ expect(warnings).toHaveLength(1);
203
+ expect(warnings[0].field).toBe("impact.performance.rating");
204
+ expect(warnings[0].message).toContain("Missing 'impact.performance.rating'");
205
+ expect(warnings[0].message).toContain("Performance column");
206
+ });
207
+
208
+ test("should warn when impact is an empty object", () => {
209
+ const frontmatter = {
210
+ title: "Sample Skill",
211
+ id: "sample-skill",
212
+ impact: {},
213
+ };
214
+
215
+ const warnings = validatePerformanceRating(frontmatter, testPath);
216
+
217
+ expect(warnings).toHaveLength(1);
218
+ expect(warnings[0].field).toBe("impact.performance.rating");
219
+ expect(warnings[0].message).toContain("Missing 'impact.performance.rating'");
220
+ });
221
+ });
222
+
223
+ describe("missing impact.performance field", () => {
224
+ test("should warn when impact.performance is undefined", () => {
225
+ const frontmatter = {
226
+ title: "Sample Skill",
227
+ id: "sample-skill",
228
+ impact: {
229
+ // performance is undefined
230
+ reliability: { rating: "high" },
231
+ },
232
+ };
233
+
234
+ const warnings = validatePerformanceRating(frontmatter, testPath);
235
+
236
+ expect(warnings).toHaveLength(1);
237
+ expect(warnings[0].field).toBe("impact.performance.rating");
238
+ expect(warnings[0].message).toContain("Missing 'impact.performance.rating'");
239
+ });
240
+
241
+ test("should warn when impact.performance is null", () => {
242
+ const frontmatter = {
243
+ title: "Sample Skill",
244
+ id: "sample-skill",
245
+ impact: {
246
+ performance: null,
247
+ },
248
+ };
249
+
250
+ const warnings = validatePerformanceRating(frontmatter, testPath);
251
+
252
+ expect(warnings).toHaveLength(1);
253
+ expect(warnings[0].field).toBe("impact.performance.rating");
254
+ expect(warnings[0].message).toContain("Missing 'impact.performance.rating'");
255
+ });
256
+
257
+ test("should warn when impact.performance is an empty object", () => {
258
+ const frontmatter = {
259
+ title: "Sample Skill",
260
+ id: "sample-skill",
261
+ impact: {
262
+ performance: {},
263
+ },
264
+ };
265
+
266
+ const warnings = validatePerformanceRating(frontmatter, testPath);
267
+
268
+ expect(warnings).toHaveLength(1);
269
+ expect(warnings[0].field).toBe("impact.performance.rating");
270
+ expect(warnings[0].message).toContain("Missing 'impact.performance.rating'");
271
+ });
272
+ });
273
+
274
+ describe("missing impact.performance.rating field", () => {
275
+ test("should warn when impact.performance.rating is undefined", () => {
276
+ const frontmatter = {
277
+ title: "Sample Skill",
278
+ id: "sample-skill",
279
+ impact: {
280
+ performance: {
281
+ // rating is undefined
282
+ description: "Some description",
283
+ },
284
+ },
285
+ };
286
+
287
+ const warnings = validatePerformanceRating(frontmatter, testPath);
288
+
289
+ expect(warnings).toHaveLength(1);
290
+ expect(warnings[0].field).toBe("impact.performance.rating");
291
+ expect(warnings[0].message).toContain("Missing 'impact.performance.rating'");
292
+ });
293
+
294
+ test("should warn when impact.performance.rating is null", () => {
295
+ const frontmatter = {
296
+ title: "Sample Skill",
297
+ id: "sample-skill",
298
+ impact: {
299
+ performance: {
300
+ rating: null,
301
+ },
302
+ },
303
+ };
304
+
305
+ const warnings = validatePerformanceRating(frontmatter, testPath);
306
+
307
+ expect(warnings).toHaveLength(1);
308
+ expect(warnings[0].field).toBe("impact.performance.rating");
309
+ expect(warnings[0].message).toContain("Missing 'impact.performance.rating'");
310
+ });
311
+
312
+ test("should warn when impact.performance.rating is an empty string", () => {
313
+ const frontmatter = {
314
+ title: "Sample Skill",
315
+ id: "sample-skill",
316
+ impact: {
317
+ performance: {
318
+ rating: "",
319
+ },
320
+ },
321
+ };
322
+
323
+ const warnings = validatePerformanceRating(frontmatter, testPath);
324
+
325
+ expect(warnings).toHaveLength(1);
326
+ expect(warnings[0].field).toBe("impact.performance.rating");
327
+ expect(warnings[0].message).toContain("Empty 'impact.performance.rating'");
328
+ });
329
+ });
330
+
331
+ describe("valid impact.performance.rating", () => {
332
+ test("should not warn when impact.performance.rating is a valid string", () => {
333
+ const frontmatter = {
334
+ title: "Sample Skill",
335
+ id: "sample-skill",
336
+ impact: {
337
+ performance: {
338
+ rating: "high",
339
+ },
340
+ },
341
+ };
342
+
343
+ const warnings = validatePerformanceRating(frontmatter, testPath);
344
+
345
+ expect(warnings).toHaveLength(0);
346
+ });
347
+
348
+ test("should not warn when impact.performance.rating is a number", () => {
349
+ const frontmatter = {
350
+ title: "Sample Skill",
351
+ id: "sample-skill",
352
+ impact: {
353
+ performance: {
354
+ rating: 5,
355
+ },
356
+ },
357
+ };
358
+
359
+ const warnings = validatePerformanceRating(frontmatter, testPath);
360
+
361
+ expect(warnings).toHaveLength(0);
362
+ });
363
+ });
364
+
365
+ describe("warning message format", () => {
366
+ test("should include correct column name in warning message", () => {
367
+ const frontmatter = {
368
+ title: "Sample Skill",
369
+ id: "sample-skill",
370
+ };
371
+
372
+ const warnings = validatePerformanceRating(frontmatter, testPath);
373
+
374
+ expect(warnings[0].message).toBe(
375
+ "Missing 'impact.performance.rating' - will show '?' in Performance column of skills index"
376
+ );
377
+ });
378
+
379
+ test("should include correct field name in warning", () => {
380
+ const frontmatter = {
381
+ title: "Sample Skill",
382
+ id: "sample-skill",
383
+ };
384
+
385
+ const warnings = validatePerformanceRating(frontmatter, testPath);
386
+
387
+ expect(warnings[0].field).toBe("impact.performance.rating");
388
+ });
389
+ });
390
+ });
391
+
392
+ describe("edge cases", () => {
393
+ test("should handle frontmatter with both fields missing", () => {
394
+ const frontmatter = {
395
+ title: "Sample Skill",
396
+ id: "sample-skill",
397
+ };
398
+
399
+ const complexityWarnings = validateComplexityLevel(frontmatter, testPath);
400
+ const performanceWarnings = validatePerformanceRating(frontmatter, testPath);
401
+
402
+ expect(complexityWarnings).toHaveLength(1);
403
+ expect(performanceWarnings).toHaveLength(1);
404
+ });
405
+
406
+ test("should handle frontmatter with both fields present", () => {
407
+ const frontmatter = {
408
+ title: "Sample Skill",
409
+ id: "sample-skill",
410
+ complexity: {
411
+ level: "intermediate",
412
+ },
413
+ impact: {
414
+ performance: {
415
+ rating: "high",
416
+ },
417
+ },
418
+ };
419
+
420
+ const complexityWarnings = validateComplexityLevel(frontmatter, testPath);
421
+ const performanceWarnings = validatePerformanceRating(frontmatter, testPath);
422
+
423
+ expect(complexityWarnings).toHaveLength(0);
424
+ expect(performanceWarnings).toHaveLength(0);
425
+ });
426
+
427
+ test("should not warn for complexity.level value of zero (uses explicit null check)", () => {
428
+ const frontmatter = {
429
+ title: "Sample Skill",
430
+ id: "sample-skill",
431
+ complexity: {
432
+ level: 0, // 0 is present (not null/undefined), so not "missing"
433
+ },
434
+ };
435
+
436
+ const warnings = validateComplexityLevel(frontmatter, testPath);
437
+
438
+ // Uses explicit null check, so 0 is treated as present (not missing)
439
+ expect(warnings).toHaveLength(0);
440
+ });
441
+
442
+ test("should not warn for impact.performance.rating value of zero (uses explicit null check)", () => {
443
+ const frontmatter = {
444
+ title: "Sample Skill",
445
+ id: "sample-skill",
446
+ impact: {
447
+ performance: {
448
+ rating: 0, // 0 is present (not null/undefined), so not "missing"
449
+ },
450
+ },
451
+ };
452
+
453
+ const warnings = validatePerformanceRating(frontmatter, testPath);
454
+
455
+ // Uses explicit null check, so 0 is treated as present (not missing)
456
+ expect(warnings).toHaveLength(0);
457
+ });
458
+
459
+ test("should not warn for complexity.level value of false (uses explicit null check)", () => {
460
+ const frontmatter = {
461
+ title: "Sample Skill",
462
+ id: "sample-skill",
463
+ complexity: {
464
+ level: false, // false is present (not null/undefined), so not "missing"
465
+ },
466
+ };
467
+
468
+ const warnings = validateComplexityLevel(frontmatter, testPath);
469
+
470
+ // Uses explicit null check, so false is treated as present (not missing)
471
+ expect(warnings).toHaveLength(0);
472
+ });
473
+
474
+ test("should not warn for impact.performance.rating value of false (uses explicit null check)", () => {
475
+ const frontmatter = {
476
+ title: "Sample Skill",
477
+ id: "sample-skill",
478
+ impact: {
479
+ performance: {
480
+ rating: false, // false is present (not null/undefined), so not "missing"
481
+ },
482
+ },
483
+ };
484
+
485
+ const warnings = validatePerformanceRating(frontmatter, testPath);
486
+
487
+ // Uses explicit null check, so false is treated as present (not missing)
488
+ expect(warnings).toHaveLength(0);
489
+ });
490
+ });
491
+
492
+ describe("exotic value types", () => {
493
+ describe("NaN values", () => {
494
+ test("should not warn for complexity.level value of NaN (is present, not null/undefined)", () => {
495
+ const frontmatter = {
496
+ title: "Sample Skill",
497
+ id: "sample-skill",
498
+ complexity: {
499
+ level: NaN, // NaN is present (not null/undefined), so not "missing"
500
+ },
501
+ };
502
+
503
+ const warnings = validateComplexityLevel(frontmatter, testPath);
504
+
505
+ // Uses explicit null check, so NaN is treated as present
506
+ expect(warnings).toHaveLength(0);
507
+ });
508
+
509
+ test("should not warn for impact.performance.rating value of NaN (is present, not null/undefined)", () => {
510
+ const frontmatter = {
511
+ title: "Sample Skill",
512
+ id: "sample-skill",
513
+ impact: {
514
+ performance: {
515
+ rating: NaN, // NaN is present (not null/undefined), so not "missing"
516
+ },
517
+ },
518
+ };
519
+
520
+ const warnings = validatePerformanceRating(frontmatter, testPath);
521
+
522
+ // Uses explicit null check, so NaN is treated as present
523
+ expect(warnings).toHaveLength(0);
524
+ });
525
+ });
526
+
527
+ describe("empty array values for scalar fields", () => {
528
+ test("should not warn for complexity.level as empty array (is present, not null/undefined)", () => {
529
+ const frontmatter = {
530
+ title: "Sample Skill",
531
+ id: "sample-skill",
532
+ complexity: {
533
+ level: [], // Empty array is present (not null/undefined)
534
+ },
535
+ };
536
+
537
+ const warnings = validateComplexityLevel(frontmatter, testPath);
538
+
539
+ // String([]) === "", but the local function only checks for null/undefined
540
+ // The actual validateSkill will produce an invalid enum warning
541
+ expect(warnings).toHaveLength(0);
542
+ });
543
+
544
+ test("should not warn for impact.performance.rating as empty array (is present, not null/undefined)", () => {
545
+ const frontmatter = {
546
+ title: "Sample Skill",
547
+ id: "sample-skill",
548
+ impact: {
549
+ performance: {
550
+ rating: [], // Empty array is present (not null/undefined)
551
+ },
552
+ },
553
+ };
554
+
555
+ const warnings = validatePerformanceRating(frontmatter, testPath);
556
+
557
+ // String([]) === "", but the local function only checks for null/undefined
558
+ expect(warnings).toHaveLength(0);
559
+ });
560
+ });
561
+
562
+ describe("object values for scalar fields", () => {
563
+ test("should not warn for complexity.level as object (is present, not null/undefined)", () => {
564
+ const frontmatter = {
565
+ title: "Sample Skill",
566
+ id: "sample-skill",
567
+ complexity: {
568
+ level: { nested: "value" }, // Object is present (not null/undefined)
569
+ },
570
+ };
571
+
572
+ const warnings = validateComplexityLevel(frontmatter, testPath);
573
+
574
+ // String({}) === "[object Object]", but the local function only checks for null/undefined
575
+ expect(warnings).toHaveLength(0);
576
+ });
577
+
578
+ test("should not warn for impact.performance.rating as object (is present, not null/undefined)", () => {
579
+ const frontmatter = {
580
+ title: "Sample Skill",
581
+ id: "sample-skill",
582
+ impact: {
583
+ performance: {
584
+ rating: { nested: "value" }, // Object is present (not null/undefined)
585
+ },
586
+ },
587
+ };
588
+
589
+ const warnings = validatePerformanceRating(frontmatter, testPath);
590
+
591
+ // String({}) === "[object Object]", but the local function only checks for null/undefined
592
+ expect(warnings).toHaveLength(0);
593
+ });
594
+ });
595
+ });
596
+
597
+ describe("impact object type validation", () => {
598
+ test("should return true for valid impact object", () => {
599
+ const frontmatter = {
600
+ impact: { performance: { rating: "high" } },
601
+ };
602
+
603
+ expect(isValidImpactObject(frontmatter)).toBe(true);
604
+ });
605
+
606
+ test("should return false for null impact", () => {
607
+ const frontmatter = {
608
+ impact: null,
609
+ };
610
+
611
+ expect(isValidImpactObject(frontmatter)).toBe(false);
612
+ });
613
+
614
+ test("should return false for undefined impact", () => {
615
+ const frontmatter = {};
616
+
617
+ expect(isValidImpactObject(frontmatter)).toBe(false);
618
+ });
619
+
620
+ test("should return false for string impact (typeof string !== object)", () => {
621
+ const frontmatter = {
622
+ impact: "not-an-object",
623
+ };
624
+
625
+ expect(isValidImpactObject(frontmatter)).toBe(false);
626
+ });
627
+
628
+ test("should return false for number impact (typeof number !== object)", () => {
629
+ const frontmatter = {
630
+ impact: 42,
631
+ };
632
+
633
+ expect(isValidImpactObject(frontmatter)).toBe(false);
634
+ });
635
+
636
+ test("should return false for boolean impact (typeof boolean !== object)", () => {
637
+ const frontmatter = {
638
+ impact: true,
639
+ };
640
+
641
+ expect(isValidImpactObject(frontmatter)).toBe(false);
642
+ });
643
+
644
+ test("should return true for empty object impact (is valid object, just empty)", () => {
645
+ const frontmatter = {
646
+ impact: {},
647
+ };
648
+
649
+ // Empty object is still a valid object to iterate over (Object.keys returns [])
650
+ expect(isValidImpactObject(frontmatter)).toBe(true);
651
+ });
652
+
653
+ test("should return false for array impact (arrays should not be valid impact objects)", () => {
654
+ const frontmatter = {
655
+ impact: [],
656
+ };
657
+
658
+ // Arrays are explicitly excluded since they would cause confusing validation
659
+ // (array indices would be treated as unknown impact types)
660
+ expect(isValidImpactObject(frontmatter)).toBe(false);
661
+ });
662
+ });
663
+ });
664
+
665
+ /**
666
+ * Integration tests using the actual validateSkill function from validate-skills.js.
667
+ *
668
+ * These tests ensure that empty string values in optional fields produce exactly ONE warning,
669
+ * not duplicates from both "invalid enum" and "missing/empty" validation paths.
670
+ */
671
+ describe("validate-skills integration tests for optional fields", () => {
672
+ const fs = require("fs");
673
+ const path = require("path");
674
+ const os = require("os");
675
+ const { validateSkill } = require("../validate-skills");
676
+
677
+ let tempDir;
678
+
679
+ beforeEach(() => {
680
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "validate-skills-test-"));
681
+ });
682
+
683
+ afterEach(() => {
684
+ if (tempDir && fs.existsSync(tempDir)) {
685
+ fs.rmSync(tempDir, { recursive: true, force: true });
686
+ }
687
+ });
688
+
689
+ /**
690
+ * Creates a skill file object with all required fields present and valid.
691
+ * @param {string} tempDir - The temporary directory path
692
+ * @param {string} content - The markdown content with frontmatter
693
+ * @returns {Object} Skill file object compatible with validateSkill()
694
+ */
695
+ function createMockSkillFile(tempDir, content) {
696
+ const fileName = "test-skill.md";
697
+ const filePath = path.join(tempDir, fileName);
698
+ fs.writeFileSync(filePath, content, "utf8");
699
+ return {
700
+ path: filePath,
701
+ relativePath: "testing/test-skill.md",
702
+ expectedId: "test-skill",
703
+ category: "testing",
704
+ };
705
+ }
706
+
707
+ /**
708
+ * Creates valid frontmatter with all required fields set.
709
+ * Override specific fields as needed.
710
+ * @param {Object} overrides - Fields to override in the frontmatter
711
+ * @returns {string} The YAML frontmatter as a string
712
+ */
713
+ function createValidFrontmatter(overrides = {}) {
714
+ const base = {
715
+ title: "Test Skill",
716
+ id: "test-skill",
717
+ category: "testing",
718
+ version: "1.0.0",
719
+ created: "2026-01-30",
720
+ updated: "2026-01-30",
721
+ status: "stable",
722
+ ...overrides,
723
+ };
724
+
725
+ let yaml = "---\n";
726
+ for (const [key, value] of Object.entries(base)) {
727
+ if (typeof value === "object" && value !== null) {
728
+ yaml += `${key}:\n`;
729
+ for (const [subKey, subValue] of Object.entries(value)) {
730
+ if (typeof subValue === "object" && subValue !== null) {
731
+ yaml += ` ${subKey}:\n`;
732
+ for (const [subSubKey, subSubValue] of Object.entries(subValue)) {
733
+ yaml += ` ${subSubKey}: "${subSubValue}"\n`;
734
+ }
735
+ } else {
736
+ yaml += ` ${subKey}: "${subValue}"\n`;
737
+ }
738
+ }
739
+ } else {
740
+ yaml += `${key}: "${value}"\n`;
741
+ }
742
+ }
743
+ yaml += "---\n\n## Overview\n\nTest content.\n\n## Solution\n\nTest solution.\n";
744
+ return yaml;
745
+ }
746
+
747
+ describe("empty string complexity.level produces exactly one warning", () => {
748
+ test("should produce exactly 1 warning for complexity.level: '' (no duplicate from invalid enum check)", () => {
749
+ const content = createValidFrontmatter({
750
+ complexity: { level: "" },
751
+ });
752
+ const skillFile = createMockSkillFile(tempDir, content);
753
+
754
+ const result = validateSkill(skillFile);
755
+
756
+ // Filter warnings for complexity.level field only
757
+ const complexityWarnings = result.warnings.filter(
758
+ (w) => w.field === "complexity.level"
759
+ );
760
+
761
+ // Should have exactly 1 warning about empty complexity.level
762
+ expect(complexityWarnings).toHaveLength(1);
763
+ expect(complexityWarnings[0].message).toContain("Empty 'complexity.level'");
764
+ expect(complexityWarnings[0].message).toContain("Complexity column");
765
+ });
766
+ });
767
+
768
+ describe("whitespace-only complexity.level produces invalid enum warning", () => {
769
+ test("should produce invalid enum warning for complexity.level: ' ' (whitespace not treated as empty)", () => {
770
+ const content = createValidFrontmatter({
771
+ complexity: { level: " " },
772
+ });
773
+ const skillFile = createMockSkillFile(tempDir, content);
774
+
775
+ const result = validateSkill(skillFile);
776
+
777
+ // Filter warnings for complexity.level field only
778
+ const complexityWarnings = result.warnings.filter(
779
+ (w) => w.field === "complexity.level"
780
+ );
781
+
782
+ // Should have exactly 1 warning about invalid enum value
783
+ // Whitespace-only strings are not trimmed, so treated as an invalid enum value
784
+ expect(complexityWarnings).toHaveLength(1);
785
+ expect(complexityWarnings[0].message).toContain("Invalid complexity level");
786
+ expect(complexityWarnings[0].message).toContain(" ");
787
+ });
788
+ });
789
+
790
+ describe("empty string impact.performance.rating produces exactly one warning", () => {
791
+ test("should produce exactly 1 warning for impact.performance.rating: '' (no duplicate from invalid enum check)", () => {
792
+ const content = createValidFrontmatter({
793
+ impact: { performance: { rating: "" } },
794
+ });
795
+ const skillFile = createMockSkillFile(tempDir, content);
796
+
797
+ const result = validateSkill(skillFile);
798
+
799
+ // Filter warnings for impact.performance.rating field only
800
+ const performanceWarnings = result.warnings.filter(
801
+ (w) => w.field === "impact.performance.rating"
802
+ );
803
+
804
+ // Should have exactly 1 warning about empty impact.performance.rating
805
+ expect(performanceWarnings).toHaveLength(1);
806
+ expect(performanceWarnings[0].message).toContain("Empty 'impact.performance.rating'");
807
+ expect(performanceWarnings[0].message).toContain("Performance column");
808
+ });
809
+ });
810
+
811
+ describe("whitespace-only impact.performance.rating produces invalid enum warning", () => {
812
+ test("should produce invalid enum warning for impact.performance.rating: ' ' (whitespace not treated as empty)", () => {
813
+ const content = createValidFrontmatter({
814
+ impact: { performance: { rating: " " } },
815
+ });
816
+ const skillFile = createMockSkillFile(tempDir, content);
817
+
818
+ const result = validateSkill(skillFile);
819
+
820
+ // Filter warnings for impact.performance.rating field only
821
+ const performanceWarnings = result.warnings.filter(
822
+ (w) => w.field === "impact.performance.rating"
823
+ );
824
+
825
+ // Should have exactly 1 warning about invalid rating
826
+ // Whitespace-only strings are not trimmed, so treated as an invalid enum value
827
+ expect(performanceWarnings).toHaveLength(1);
828
+ expect(performanceWarnings[0].message).toContain("Invalid rating");
829
+ expect(performanceWarnings[0].message).toContain(" ");
830
+ });
831
+ });
832
+
833
+ describe("valid enum values produce no warnings for those fields", () => {
834
+ test("should produce no warnings for valid complexity.level value", () => {
835
+ const content = createValidFrontmatter({
836
+ complexity: { level: "intermediate" },
837
+ impact: { performance: { rating: "high" } },
838
+ });
839
+ const skillFile = createMockSkillFile(tempDir, content);
840
+
841
+ const result = validateSkill(skillFile);
842
+
843
+ // Should have no warnings for complexity.level or performance.rating
844
+ const relevantWarnings = result.warnings.filter(
845
+ (w) => w.field === "complexity.level" || w.field === "impact.performance.rating"
846
+ );
847
+ expect(relevantWarnings).toHaveLength(0);
848
+ });
849
+ });
850
+
851
+ describe("invalid enum values produce exactly one warning", () => {
852
+ test("should produce exactly 1 warning for invalid complexity.level enum value", () => {
853
+ const content = createValidFrontmatter({
854
+ complexity: { level: "super-hard" },
855
+ });
856
+ const skillFile = createMockSkillFile(tempDir, content);
857
+
858
+ const result = validateSkill(skillFile);
859
+
860
+ // Filter warnings for complexity.level field only
861
+ const complexityWarnings = result.warnings.filter(
862
+ (w) => w.field === "complexity.level"
863
+ );
864
+
865
+ // Should have exactly 1 warning about invalid enum value (not 2)
866
+ expect(complexityWarnings).toHaveLength(1);
867
+ expect(complexityWarnings[0].message).toContain("Invalid complexity level");
868
+ expect(complexityWarnings[0].message).toContain("super-hard");
869
+ });
870
+
871
+ test("should produce exactly 1 warning for invalid impact.performance.rating enum value", () => {
872
+ const content = createValidFrontmatter({
873
+ impact: { performance: { rating: "super-high" } },
874
+ });
875
+ const skillFile = createMockSkillFile(tempDir, content);
876
+
877
+ const result = validateSkill(skillFile);
878
+
879
+ // Filter warnings for impact.performance.rating field only
880
+ const performanceWarnings = result.warnings.filter(
881
+ (w) => w.field === "impact.performance.rating"
882
+ );
883
+
884
+ // Should have exactly 1 warning about invalid rating (not 2)
885
+ expect(performanceWarnings).toHaveLength(1);
886
+ expect(performanceWarnings[0].message).toContain("Invalid rating");
887
+ expect(performanceWarnings[0].message).toContain("super-high");
888
+ });
889
+ });
890
+
891
+ describe("exotic value types produce meaningful warnings via String coercion", () => {
892
+ test("should produce warning for version as NaN (String(NaN) === 'NaN' does not match semver)", () => {
893
+ // NaN cannot be directly serialized in YAML, but we test the validation logic
894
+ const content = `---
895
+ title: "Test Skill"
896
+ id: "test-skill"
897
+ category: "testing"
898
+ version: ".nan"
899
+ created: "2026-01-30"
900
+ updated: "2026-01-30"
901
+ status: "stable"
902
+ ---
903
+
904
+ ## Overview
905
+
906
+ Test content.
907
+
908
+ ## Solution
909
+
910
+ Test solution.
911
+ `;
912
+ const skillFile = createMockSkillFile(tempDir, content);
913
+
914
+ const result = validateSkill(skillFile);
915
+
916
+ // Filter warnings for version field only
917
+ const versionWarnings = result.warnings.filter((w) => w.field === "version");
918
+
919
+ // String(".nan") does not match semver pattern
920
+ expect(versionWarnings).toHaveLength(1);
921
+ expect(versionWarnings[0].message).toContain("should be in semver format");
922
+ });
923
+
924
+ test("should produce warning for version as object-like string (simulating String({}) coercion)", () => {
925
+ const content = `---
926
+ title: "Test Skill"
927
+ id: "test-skill"
928
+ category: "testing"
929
+ version: "[object Object]"
930
+ created: "2026-01-30"
931
+ updated: "2026-01-30"
932
+ status: "stable"
933
+ ---
934
+
935
+ ## Overview
936
+
937
+ Test content.
938
+
939
+ ## Solution
940
+
941
+ Test solution.
942
+ `;
943
+ const skillFile = createMockSkillFile(tempDir, content);
944
+
945
+ const result = validateSkill(skillFile);
946
+
947
+ // Filter warnings for version field only
948
+ const versionWarnings = result.warnings.filter((w) => w.field === "version");
949
+
950
+ // String({}) === "[object Object]" does not match semver pattern
951
+ expect(versionWarnings).toHaveLength(1);
952
+ expect(versionWarnings[0].message).toContain("[object Object]");
953
+ expect(versionWarnings[0].message).toContain("should be in semver format");
954
+ });
955
+
956
+ test("should produce warning for created date as invalid format (simulating String([]) coercion)", () => {
957
+ // YAML parses [] as an empty array, which when stringified becomes ""
958
+ // For testing, we use a string that shows the behavior
959
+ const content = `---
960
+ title: "Test Skill"
961
+ id: "test-skill"
962
+ category: "testing"
963
+ version: "1.0.0"
964
+ created: "[]"
965
+ updated: "2026-01-30"
966
+ status: "stable"
967
+ ---
968
+
969
+ ## Overview
970
+
971
+ Test content.
972
+
973
+ ## Solution
974
+
975
+ Test solution.
976
+ `;
977
+ const skillFile = createMockSkillFile(tempDir, content);
978
+
979
+ const result = validateSkill(skillFile);
980
+
981
+ // Filter warnings for created field only
982
+ const createdWarnings = result.warnings.filter((w) => w.field === "created");
983
+
984
+ // "[]" does not match ISO date pattern YYYY-MM-DD
985
+ expect(createdWarnings).toHaveLength(1);
986
+ expect(createdWarnings[0].message).toContain("should be in ISO format");
987
+ });
988
+
989
+ test("should handle impact as non-object type gracefully (no crash)", () => {
990
+ // When impact is a string, the typeof check should prevent Object.keys from crashing
991
+ const content = `---
992
+ title: "Test Skill"
993
+ id: "test-skill"
994
+ category: "testing"
995
+ version: "1.0.0"
996
+ created: "2026-01-30"
997
+ updated: "2026-01-30"
998
+ status: "stable"
999
+ impact: "not-an-object"
1000
+ ---
1001
+
1002
+ ## Overview
1003
+
1004
+ Test content.
1005
+
1006
+ ## Solution
1007
+
1008
+ Test solution.
1009
+ `;
1010
+ const skillFile = createMockSkillFile(tempDir, content);
1011
+
1012
+ // Should not throw - the typeof check prevents iteration over non-objects
1013
+ const result = validateSkill(skillFile);
1014
+
1015
+ // Should have warning about missing impact.performance.rating (since impact is not an object)
1016
+ const impactWarnings = result.warnings.filter(
1017
+ (w) => w.field === "impact.performance.rating"
1018
+ );
1019
+ expect(impactWarnings).toHaveLength(1);
1020
+ expect(impactWarnings[0].message).toContain("Missing 'impact.performance.rating'");
1021
+ });
1022
+
1023
+ test("should handle impact as number gracefully (no crash)", () => {
1024
+ const content = `---
1025
+ title: "Test Skill"
1026
+ id: "test-skill"
1027
+ category: "testing"
1028
+ version: "1.0.0"
1029
+ created: "2026-01-30"
1030
+ updated: "2026-01-30"
1031
+ status: "stable"
1032
+ impact: 42
1033
+ ---
1034
+
1035
+ ## Overview
1036
+
1037
+ Test content.
1038
+
1039
+ ## Solution
1040
+
1041
+ Test solution.
1042
+ `;
1043
+ const skillFile = createMockSkillFile(tempDir, content);
1044
+
1045
+ // Should not throw - the typeof check prevents iteration over non-objects
1046
+ const result = validateSkill(skillFile);
1047
+
1048
+ // Should have warning about missing impact.performance.rating (since impact is not an object)
1049
+ const impactWarnings = result.warnings.filter(
1050
+ (w) => w.field === "impact.performance.rating"
1051
+ );
1052
+ expect(impactWarnings).toHaveLength(1);
1053
+ });
1054
+
1055
+ test("should handle impact as boolean gracefully (no crash)", () => {
1056
+ const content = `---
1057
+ title: "Test Skill"
1058
+ id: "test-skill"
1059
+ category: "testing"
1060
+ version: "1.0.0"
1061
+ created: "2026-01-30"
1062
+ updated: "2026-01-30"
1063
+ status: "stable"
1064
+ impact: true
1065
+ ---
1066
+
1067
+ ## Overview
1068
+
1069
+ Test content.
1070
+
1071
+ ## Solution
1072
+
1073
+ Test solution.
1074
+ `;
1075
+ const skillFile = createMockSkillFile(tempDir, content);
1076
+
1077
+ // Should not throw - the typeof check prevents iteration over non-objects
1078
+ const result = validateSkill(skillFile);
1079
+
1080
+ // Should have warning about missing impact.performance.rating (since impact is not an object)
1081
+ const impactWarnings = result.warnings.filter(
1082
+ (w) => w.field === "impact.performance.rating"
1083
+ );
1084
+ expect(impactWarnings).toHaveLength(1);
1085
+ });
1086
+ });
1087
+
1088
+ describe("exotic value types for version and date fields", () => {
1089
+ describe("NaN values", () => {
1090
+ test("should produce warning for version field containing YAML NaN (.nan)", () => {
1091
+ // YAML parses .nan as NaN, which String() converts to "NaN"
1092
+ const content = `---
1093
+ title: "Test Skill"
1094
+ id: "test-skill"
1095
+ category: "testing"
1096
+ version: .nan
1097
+ created: "2026-01-30"
1098
+ updated: "2026-01-30"
1099
+ status: "stable"
1100
+ ---
1101
+
1102
+ ## Overview
1103
+
1104
+ Test content.
1105
+
1106
+ ## Solution
1107
+
1108
+ Test solution.
1109
+ `;
1110
+ const skillFile = createMockSkillFile(tempDir, content);
1111
+
1112
+ const result = validateSkill(skillFile);
1113
+
1114
+ const versionWarnings = result.warnings.filter((w) => w.field === "version");
1115
+
1116
+ // String(NaN) === "NaN" does not match semver pattern
1117
+ expect(versionWarnings).toHaveLength(1);
1118
+ expect(versionWarnings[0].message).toContain("should be in semver format");
1119
+ });
1120
+
1121
+ test("should produce warning for created date field containing YAML NaN (.nan)", () => {
1122
+ const content = `---
1123
+ title: "Test Skill"
1124
+ id: "test-skill"
1125
+ category: "testing"
1126
+ version: "1.0.0"
1127
+ created: .nan
1128
+ updated: "2026-01-30"
1129
+ status: "stable"
1130
+ ---
1131
+
1132
+ ## Overview
1133
+
1134
+ Test content.
1135
+
1136
+ ## Solution
1137
+
1138
+ Test solution.
1139
+ `;
1140
+ const skillFile = createMockSkillFile(tempDir, content);
1141
+
1142
+ const result = validateSkill(skillFile);
1143
+
1144
+ const createdWarnings = result.warnings.filter((w) => w.field === "created");
1145
+
1146
+ // String(NaN) === "NaN" does not match ISO date pattern
1147
+ expect(createdWarnings).toHaveLength(1);
1148
+ expect(createdWarnings[0].message).toContain("should be in ISO format");
1149
+ });
1150
+
1151
+ test("should produce warning for updated date field containing YAML NaN (.nan)", () => {
1152
+ const content = `---
1153
+ title: "Test Skill"
1154
+ id: "test-skill"
1155
+ category: "testing"
1156
+ version: "1.0.0"
1157
+ created: "2026-01-30"
1158
+ updated: .nan
1159
+ status: "stable"
1160
+ ---
1161
+
1162
+ ## Overview
1163
+
1164
+ Test content.
1165
+
1166
+ ## Solution
1167
+
1168
+ Test solution.
1169
+ `;
1170
+ const skillFile = createMockSkillFile(tempDir, content);
1171
+
1172
+ const result = validateSkill(skillFile);
1173
+
1174
+ const updatedWarnings = result.warnings.filter((w) => w.field === "updated");
1175
+
1176
+ // String(NaN) === "NaN" does not match ISO date pattern
1177
+ expect(updatedWarnings).toHaveLength(1);
1178
+ expect(updatedWarnings[0].message).toContain("should be in ISO format");
1179
+ });
1180
+ });
1181
+
1182
+ describe("empty array values (YAML parses [] as empty array, String([]) is empty)", () => {
1183
+ test("should produce warning for version as empty array (empty string is invalid semver)", () => {
1184
+ // YAML parses [] as an empty array; String([]) === ""
1185
+ // The value is truthy (not null/undefined), so validation runs
1186
+ const content = `---
1187
+ title: "Test Skill"
1188
+ id: "test-skill"
1189
+ category: "testing"
1190
+ version: []
1191
+ created: "2026-01-30"
1192
+ updated: "2026-01-30"
1193
+ status: "stable"
1194
+ ---
1195
+
1196
+ ## Overview
1197
+
1198
+ Test content.
1199
+
1200
+ ## Solution
1201
+
1202
+ Test solution.
1203
+ `;
1204
+ const skillFile = createMockSkillFile(tempDir, content);
1205
+
1206
+ const result = validateSkill(skillFile);
1207
+
1208
+ const versionWarnings = result.warnings.filter((w) => w.field === "version");
1209
+
1210
+ // YAML parses [] as empty array, which is truthy (not null/undefined)
1211
+ // String([]) === "" produces invalid semver format warning
1212
+ expect(versionWarnings).toHaveLength(1);
1213
+ expect(versionWarnings[0].message).toContain("should be in semver format");
1214
+ });
1215
+
1216
+ test("should produce warning for created date as empty array (empty string is invalid date format)", () => {
1217
+ const content = `---
1218
+ title: "Test Skill"
1219
+ id: "test-skill"
1220
+ category: "testing"
1221
+ version: "1.0.0"
1222
+ created: []
1223
+ updated: "2026-01-30"
1224
+ status: "stable"
1225
+ ---
1226
+
1227
+ ## Overview
1228
+
1229
+ Test content.
1230
+
1231
+ ## Solution
1232
+
1233
+ Test solution.
1234
+ `;
1235
+ const skillFile = createMockSkillFile(tempDir, content);
1236
+
1237
+ const result = validateSkill(skillFile);
1238
+
1239
+ const createdWarnings = result.warnings.filter((w) => w.field === "created");
1240
+
1241
+ // YAML parses [] as empty array which is truthy (not null/undefined)
1242
+ // The validation runs and String([]) === "" produces a warning
1243
+ expect(createdWarnings).toHaveLength(1);
1244
+ expect(createdWarnings[0].message).toContain("should be in ISO format");
1245
+ });
1246
+
1247
+ test("should produce warning for updated date as empty array (empty string is invalid date format)", () => {
1248
+ const content = `---
1249
+ title: "Test Skill"
1250
+ id: "test-skill"
1251
+ category: "testing"
1252
+ version: "1.0.0"
1253
+ created: "2026-01-30"
1254
+ updated: []
1255
+ status: "stable"
1256
+ ---
1257
+
1258
+ ## Overview
1259
+
1260
+ Test content.
1261
+
1262
+ ## Solution
1263
+
1264
+ Test solution.
1265
+ `;
1266
+ const skillFile = createMockSkillFile(tempDir, content);
1267
+
1268
+ const result = validateSkill(skillFile);
1269
+
1270
+ const updatedWarnings = result.warnings.filter((w) => w.field === "updated");
1271
+
1272
+ // YAML parses [] as empty array which is truthy (not null/undefined)
1273
+ // The validation runs and String([]) === "" produces a warning
1274
+ expect(updatedWarnings).toHaveLength(1);
1275
+ expect(updatedWarnings[0].message).toContain("should be in ISO format");
1276
+ });
1277
+ });
1278
+
1279
+ describe("object values (YAML parses {} as empty object)", () => {
1280
+ test("should produce warning for version as empty object (invalid semver)", () => {
1281
+ // YAML parses {} as an empty object
1282
+ // String({}) in JavaScript is "[object Object]" but YAML/gray-matter may stringify differently
1283
+ const content = `---
1284
+ title: "Test Skill"
1285
+ id: "test-skill"
1286
+ category: "testing"
1287
+ version: {}
1288
+ created: "2026-01-30"
1289
+ updated: "2026-01-30"
1290
+ status: "stable"
1291
+ ---
1292
+
1293
+ ## Overview
1294
+
1295
+ Test content.
1296
+
1297
+ ## Solution
1298
+
1299
+ Test solution.
1300
+ `;
1301
+ const skillFile = createMockSkillFile(tempDir, content);
1302
+
1303
+ const result = validateSkill(skillFile);
1304
+
1305
+ const versionWarnings = result.warnings.filter((w) => w.field === "version");
1306
+
1307
+ // Empty object does not match semver pattern
1308
+ expect(versionWarnings).toHaveLength(1);
1309
+ expect(versionWarnings[0].message).toContain("should be in semver format");
1310
+ });
1311
+
1312
+ test("should produce warning for created date as empty object (invalid date format)", () => {
1313
+ const content = `---
1314
+ title: "Test Skill"
1315
+ id: "test-skill"
1316
+ category: "testing"
1317
+ version: "1.0.0"
1318
+ created: {}
1319
+ updated: "2026-01-30"
1320
+ status: "stable"
1321
+ ---
1322
+
1323
+ ## Overview
1324
+
1325
+ Test content.
1326
+
1327
+ ## Solution
1328
+
1329
+ Test solution.
1330
+ `;
1331
+ const skillFile = createMockSkillFile(tempDir, content);
1332
+
1333
+ const result = validateSkill(skillFile);
1334
+
1335
+ const createdWarnings = result.warnings.filter((w) => w.field === "created");
1336
+
1337
+ // Empty object does not match ISO date pattern
1338
+ expect(createdWarnings).toHaveLength(1);
1339
+ expect(createdWarnings[0].message).toContain("should be in ISO format");
1340
+ });
1341
+
1342
+ test("should produce warning for updated date as empty object (invalid date format)", () => {
1343
+ const content = `---
1344
+ title: "Test Skill"
1345
+ id: "test-skill"
1346
+ category: "testing"
1347
+ version: "1.0.0"
1348
+ created: "2026-01-30"
1349
+ updated: {}
1350
+ status: "stable"
1351
+ ---
1352
+
1353
+ ## Overview
1354
+
1355
+ Test content.
1356
+
1357
+ ## Solution
1358
+
1359
+ Test solution.
1360
+ `;
1361
+ const skillFile = createMockSkillFile(tempDir, content);
1362
+
1363
+ const result = validateSkill(skillFile);
1364
+
1365
+ const updatedWarnings = result.warnings.filter((w) => w.field === "updated");
1366
+
1367
+ // Empty object does not match ISO date pattern
1368
+ expect(updatedWarnings).toHaveLength(1);
1369
+ expect(updatedWarnings[0].message).toContain("should be in ISO format");
1370
+ });
1371
+ });
1372
+
1373
+ describe("non-empty array values (String([1,2]) coerces to '1,2')", () => {
1374
+ test("should produce warning for version as non-empty array (invalid semver)", () => {
1375
+ const content = `---
1376
+ title: "Test Skill"
1377
+ id: "test-skill"
1378
+ category: "testing"
1379
+ version:
1380
+ - 1
1381
+ - 0
1382
+ - 0
1383
+ created: "2026-01-30"
1384
+ updated: "2026-01-30"
1385
+ status: "stable"
1386
+ ---
1387
+
1388
+ ## Overview
1389
+
1390
+ Test content.
1391
+
1392
+ ## Solution
1393
+
1394
+ Test solution.
1395
+ `;
1396
+ const skillFile = createMockSkillFile(tempDir, content);
1397
+
1398
+ const result = validateSkill(skillFile);
1399
+
1400
+ const versionWarnings = result.warnings.filter((w) => w.field === "version");
1401
+
1402
+ // String([1, 0, 0]) === "1,0,0" does not match semver pattern (needs dots, not commas)
1403
+ expect(versionWarnings).toHaveLength(1);
1404
+ expect(versionWarnings[0].message).toContain("should be in semver format");
1405
+ });
1406
+
1407
+ test("should produce warning for created date as non-empty array (invalid date)", () => {
1408
+ const content = `---
1409
+ title: "Test Skill"
1410
+ id: "test-skill"
1411
+ category: "testing"
1412
+ version: "1.0.0"
1413
+ created:
1414
+ - 2026
1415
+ - 01
1416
+ - 30
1417
+ updated: "2026-01-30"
1418
+ status: "stable"
1419
+ ---
1420
+
1421
+ ## Overview
1422
+
1423
+ Test content.
1424
+
1425
+ ## Solution
1426
+
1427
+ Test solution.
1428
+ `;
1429
+ const skillFile = createMockSkillFile(tempDir, content);
1430
+
1431
+ const result = validateSkill(skillFile);
1432
+
1433
+ const createdWarnings = result.warnings.filter((w) => w.field === "created");
1434
+
1435
+ // String([2026, 1, 30]) === "2026,1,30" does not match ISO date pattern
1436
+ expect(createdWarnings).toHaveLength(1);
1437
+ expect(createdWarnings[0].message).toContain("should be in ISO format");
1438
+ });
1439
+
1440
+ test("should produce warning for updated date as non-empty array (invalid date)", () => {
1441
+ const content = `---
1442
+ title: "Test Skill"
1443
+ id: "test-skill"
1444
+ category: "testing"
1445
+ version: "1.0.0"
1446
+ created: "2026-01-30"
1447
+ updated:
1448
+ - 2026
1449
+ - 01
1450
+ - 30
1451
+ status: "stable"
1452
+ ---
1453
+
1454
+ ## Overview
1455
+
1456
+ Test content.
1457
+
1458
+ ## Solution
1459
+
1460
+ Test solution.
1461
+ `;
1462
+ const skillFile = createMockSkillFile(tempDir, content);
1463
+
1464
+ const result = validateSkill(skillFile);
1465
+
1466
+ const updatedWarnings = result.warnings.filter((w) => w.field === "updated");
1467
+
1468
+ // String([2026, 1, 30]) === "2026,1,30" does not match ISO date pattern
1469
+ expect(updatedWarnings).toHaveLength(1);
1470
+ expect(updatedWarnings[0].message).toContain("should be in ISO format");
1471
+ });
1472
+ });
1473
+ });
1474
+ });