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.
- package/.artifacts/SourceGenerators.Tests/obj/Debug/net9.0/WallstopStudios.DxMessaging.SourceGenerators.Tests.AssemblyInfo.cs +1 -1
- package/.cspell.json +4 -1
- package/.github/workflows/actionlint.yml +11 -1
- package/.github/workflows/csharpier-autofix.yml +34 -3
- package/.github/workflows/dotnet-tests.yml +13 -0
- package/.github/workflows/format-on-demand.yml +38 -44
- package/.github/workflows/json-format-check.yml +24 -0
- package/.github/workflows/lint-doc-links.yml +13 -0
- package/.github/workflows/markdown-json.yml +21 -4
- package/.github/workflows/markdown-link-text-check.yml +10 -0
- package/.github/workflows/markdown-link-validity.yml +10 -0
- package/.github/workflows/markdownlint.yml +7 -5
- package/.github/workflows/prettier-autofix.yml +67 -11
- package/.github/workflows/release-drafter.yml +2 -2
- package/.github/workflows/sync-wiki.yml +3 -3
- package/.github/workflows/yaml-format-lint.yml +26 -0
- package/.llm/context.md +113 -3
- package/.llm/skills/documentation/changelog-management.md +38 -0
- package/.llm/skills/documentation/documentation-style-guide.md +18 -0
- package/.llm/skills/documentation/documentation-update-workflow.md +2 -0
- package/.llm/skills/documentation/documentation-updates.md +2 -0
- package/.llm/skills/documentation/markdown-compatibility.md +476 -0
- package/.llm/skills/documentation/mermaid-theming.md +326 -0
- package/.llm/skills/documentation/mkdocs-navigation.md +290 -0
- package/.llm/skills/github-actions/git-renormalize-patterns.md +231 -0
- package/.llm/skills/github-actions/workflow-consistency.md +346 -0
- package/.llm/skills/index.md +53 -27
- package/.llm/skills/scripting/javascript-code-quality.md +417 -0
- package/.llm/skills/scripting/regex-documentation.md +461 -0
- package/.llm/skills/scripting/shell-best-practices.md +55 -4
- package/.llm/skills/scripting/validation-patterns.md +418 -0
- package/.llm/skills/specification.md +4 -1
- package/.llm/skills/testing/test-code-quality.md +243 -0
- package/.llm/skills/testing/test-production-code.md +348 -0
- package/CHANGELOG.md +11 -0
- package/README.md +0 -11
- package/Tests/Runtime/Benchmarks/WallstopStudios.DxMessaging.Tests.Runtime.Benchmarks.asmdef +1 -6
- package/Tests/Runtime/Integrations/Reflex/WallstopStudios.DxMessaging.Tests.Runtime.Reflex.asmdef +1 -1
- package/Tests/Runtime/Integrations/VContainer/WallstopStudios.DxMessaging.Tests.Runtime.VContainer.asmdef +1 -1
- package/Tests/Runtime/Integrations/Zenject/WallstopStudios.DxMessaging.Tests.Runtime.Zenject.asmdef +1 -1
- package/coverage/clover.xml +216 -3
- package/coverage/clover.xml.meta +7 -7
- package/coverage/coverage-final.json +2 -1
- package/coverage/coverage-final.json.meta +7 -7
- package/coverage/lcov-report/base.css.meta +1 -1
- package/coverage/lcov-report/block-navigation.js.meta +1 -1
- package/coverage/lcov-report/favicon.png.meta +1 -1
- package/coverage/lcov-report/index.html +25 -10
- package/coverage/lcov-report/index.html.meta +7 -7
- package/coverage/lcov-report/prettify.css.meta +1 -1
- package/coverage/lcov-report/prettify.js.meta +1 -1
- package/coverage/lcov-report/sort-arrow-sprite.png.meta +1 -1
- package/coverage/lcov-report/sorter.js.meta +1 -1
- package/coverage/lcov-report/transform-docs-to-wiki.js.html +1 -1
- package/coverage/lcov-report/transform-docs-to-wiki.js.html.meta +7 -7
- package/coverage/lcov-report/vendor.meta +1 -1
- package/coverage/lcov-report.meta +8 -8
- package/coverage/lcov.info +365 -0
- package/coverage/lcov.info.meta +7 -7
- package/docs/architecture/design-and-architecture.md +0 -1
- package/docs/concepts/index.md +37 -0
- package/docs/concepts/index.md.meta +7 -0
- package/docs/concepts/interceptors-and-ordering.md +0 -2
- package/docs/concepts/mental-model.md +390 -0
- package/docs/concepts/mental-model.md.meta +7 -0
- package/docs/concepts/message-types.md +0 -1
- package/docs/getting-started/getting-started.md +1 -0
- package/docs/getting-started/index.md +6 -5
- package/docs/getting-started/overview.md +1 -0
- package/docs/getting-started/quick-start.md +2 -1
- package/docs/getting-started/visual-guide.md +4 -10
- package/docs/hooks.py +10 -1
- package/docs/images/DxMessaging-banner.svg +1 -1
- package/docs/index.md +7 -7
- package/docs/javascripts/mermaid-config.js +44 -4
- package/docs/reference/helpers.md +130 -154
- package/docs/reference/quick-reference.md +5 -1
- package/docs/reference/reference.md +124 -130
- package/mkdocs.yml +2 -0
- package/package.json +1 -1
- package/scripts/__tests__/generate-skills-index.test.js +397 -0
- package/scripts/__tests__/generate-skills-index.test.js.meta +7 -0
- package/scripts/__tests__/mermaid-config.test.js +467 -0
- package/scripts/__tests__/mermaid-config.test.js.meta +7 -0
- package/scripts/__tests__/validate-skills-optional-fields.test.js +1474 -0
- package/scripts/__tests__/validate-skills-optional-fields.test.js.meta +7 -0
- package/scripts/__tests__/validate-skills-required-fields.test.js +188 -0
- package/scripts/__tests__/validate-skills-required-fields.test.js.meta +7 -0
- package/scripts/__tests__/validate-skills-tags.test.js +353 -0
- package/scripts/__tests__/validate-skills-tags.test.js.meta +7 -0
- package/scripts/__tests__/validate-workflows.test.js +188 -0
- package/scripts/__tests__/validate-workflows.test.js.meta +7 -0
- package/scripts/generate-skills-index.js +88 -3
- package/scripts/validate-skills.js +230 -30
- package/scripts/validate-workflows.js +272 -0
- package/scripts/validate-workflows.js.meta +7 -0
- package/site/404.html +1 -1
- package/site/advanced/emit-shorthands/index.html +2 -2
- package/site/advanced/message-bus-providers/index.html +2 -2
- package/site/advanced/registration-builders/index.html +2 -2
- package/site/advanced/runtime-configuration/index.html +2 -2
- package/site/advanced/string-messages/index.html +2 -2
- package/site/advanced.meta +1 -1
- package/site/architecture/comparisons/index.html +2 -2
- package/site/architecture/design-and-architecture/index.html +2 -2
- package/site/architecture/performance/index.html +1 -1
- package/site/architecture.meta +1 -1
- package/site/concepts/index.html +1 -0
- package/site/concepts/index.html.meta +7 -0
- package/site/concepts/interceptors-and-ordering/index.html +4 -4
- package/site/concepts/listening-patterns/index.html +2 -2
- package/site/concepts/mental-model/index.html +146 -0
- package/site/concepts/mental-model/index.html.meta +7 -0
- package/site/concepts/mental-model.meta +8 -0
- package/site/concepts/message-types/index.html +2 -2
- package/site/concepts/targeting-and-context/index.html +2 -2
- package/site/concepts.meta +1 -1
- package/site/examples/end-to-end/index.html +2 -2
- package/site/examples/end-to-end-scene-transitions/index.html +2 -2
- package/site/examples.meta +1 -1
- package/site/getting-started/getting-started/index.html +3 -3
- package/site/getting-started/index.html +4 -4
- package/site/getting-started/install/index.html +3 -3
- package/site/getting-started/overview/index.html +2 -2
- package/site/getting-started/quick-start/index.html +2 -2
- package/site/getting-started/visual-guide/index.html +11 -11
- package/site/getting-started.meta +1 -1
- package/site/guides/advanced/index.html +2 -2
- package/site/guides/diagnostics/index.html +2 -2
- package/site/guides/migration-guide/index.html +2 -2
- package/site/guides/patterns/index.html +2 -2
- package/site/guides/testing/index.html +2 -2
- package/site/guides/unity-integration/index.html +2 -2
- package/site/guides.meta +1 -1
- package/site/hooks.py.meta +1 -1
- package/site/images/DxMessaging-banner.svg +119 -0
- package/site/images/DxMessaging-banner.svg.meta +7 -0
- package/site/images.meta +8 -0
- package/site/index.html +2 -2
- package/site/integrations/index.html +2 -2
- package/site/integrations/reflex/index.html +2 -2
- package/site/integrations/vcontainer/index.html +2 -2
- package/site/integrations/zenject/index.html +2 -2
- package/site/integrations.meta +1 -1
- package/site/javascripts/csharp-highlight.js.meta +7 -7
- package/site/javascripts/mermaid-config.js +4 -1
- package/site/javascripts/mermaid-config.js.meta +1 -1
- package/site/javascripts.meta +1 -1
- package/site/reference/compatibility/index.html +1 -1
- package/site/reference/faq/index.html +1 -1
- package/site/reference/glossary/index.html +2 -2
- package/site/reference/helpers/index.html +15 -15
- package/site/reference/quick-reference/index.html +3 -3
- package/site/reference/reference/index.html +37 -37
- package/site/reference/troubleshooting/index.html +1 -1
- package/site/reference.meta +1 -1
- package/site/search/search_index.json +1 -1
- package/site/sitemap.xml +46 -38
- package/site/sitemap.xml.gz +0 -0
- package/site/stylesheets/extra.css.meta +1 -1
- 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
|
+
});
|