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,418 @@
1
+ ---
2
+ title: "Validation Patterns and Duplicate Warning Prevention"
3
+ id: "validation-patterns"
4
+ category: "scripting"
5
+ version: "1.1.0"
6
+ created: "2026-01-30"
7
+ updated: "2026-01-30"
8
+
9
+ source:
10
+ repository: "wallstop/DxMessaging"
11
+ files:
12
+ - path: "scripts/validate-skills.js"
13
+ - path: "scripts/__tests__/validate-skills-optional-fields.test.js"
14
+ url: "https://github.com/wallstop/DxMessaging"
15
+
16
+ tags:
17
+ - "validation"
18
+ - "javascript"
19
+ - "error-handling"
20
+ - "duplicate-warnings"
21
+ - "enum-validation"
22
+ - "optional-fields"
23
+ - "testing"
24
+ - "truthiness"
25
+ - "type-coercion"
26
+
27
+ complexity:
28
+ level: "intermediate"
29
+ reasoning: "Requires understanding of validation flow, conditional logic ordering, and JavaScript type coercion"
30
+
31
+ impact:
32
+ performance:
33
+ rating: "none"
34
+ details: "Validation patterns only; no runtime performance impact"
35
+ maintainability:
36
+ rating: "high"
37
+ details: "Prevents confusing duplicate warnings that obscure actual issues"
38
+ testability:
39
+ rating: "high"
40
+ details: "Clear validation logic is easier to test exhaustively"
41
+
42
+ prerequisites:
43
+ - "JavaScript fundamentals"
44
+ - "Understanding of null/undefined distinction"
45
+ - "Basic validation concepts"
46
+
47
+ dependencies:
48
+ skills:
49
+ - "javascript-code-quality"
50
+
51
+ applies_to:
52
+ languages:
53
+ - "JavaScript"
54
+ - "TypeScript"
55
+ frameworks:
56
+ - "Node.js"
57
+
58
+ aliases:
59
+ - "duplicate warning prevention"
60
+ - "validation deduplication"
61
+ - "enum validation"
62
+ - "presence vs truthiness"
63
+
64
+ related:
65
+ - "javascript-code-quality"
66
+ - "comprehensive-test-coverage"
67
+
68
+ status: "stable"
69
+ ---
70
+
71
+ # Validation Patterns and Duplicate Warning Prevention
72
+
73
+ > **One-line summary**: Structure validation logic to produce exactly one warning per issue,
74
+ > not duplicates from multiple overlapping checks.
75
+
76
+ ## Overview
77
+
78
+ When validating fields that have multiple possible error conditions (missing, empty, invalid enum
79
+ value), careless ordering can produce duplicate warnings for the same underlying issue. This
80
+ confuses users and makes debugging harder.
81
+
82
+ ## Solution
83
+
84
+ 1. **Order validation checks from most specific to most general** - check for missing/empty before
85
+ checking for invalid values
86
+ 1. **Use else-if chains** to ensure only one warning per field
87
+ 1. **Exclude empty/null values from enum validation** - an empty string is "empty," not "invalid enum"
88
+ 1. **Write integration tests** that verify exactly one warning per field condition
89
+ 1. **Use explicit presence checks** - avoid truthiness-based validation that conflates different issues
90
+
91
+ ## Truthiness vs Presence Checks
92
+
93
+ ### The Anti-Pattern: Using Truthiness for Presence
94
+
95
+ Truthiness-based checks (`!value`, `if (value)`) conflate multiple distinct conditions, producing
96
+ misleading error messages and duplicate warnings.
97
+
98
+ ```javascript
99
+ // WRONG: Truthiness-based validation
100
+ function validateField(frontmatter, field) {
101
+ if (!frontmatter[field]) {
102
+ // This triggers for:
103
+ // - undefined (missing)
104
+ // - null (missing)
105
+ // - "" (empty string - should be separate error)
106
+ // - 0 (legitimate value for numeric fields!)
107
+ // - false (legitimate value for boolean fields!)
108
+ errors.push(`Required field '${field}' is missing`);
109
+ }
110
+ }
111
+ ```
112
+
113
+ ### The Correct Pattern: Explicit Presence Checks
114
+
115
+ Use explicit null/undefined checks, then separate empty string checks:
116
+
117
+ ```javascript
118
+ // RIGHT: Presence-based validation with explicit checks
119
+ function validateField(frontmatter, field) {
120
+ if (frontmatter[field] === undefined || frontmatter[field] === null) {
121
+ errors.push(`Required field '${field}' is missing`);
122
+ } else if (frontmatter[field] === "") {
123
+ errors.push(`Required field '${field}' is empty`);
124
+ }
125
+ }
126
+
127
+ // ALSO RIGHT: Using loose equality shorthand (null == undefined is true)
128
+ function validateField(frontmatter, field) {
129
+ if (frontmatter[field] == null) {
130
+ errors.push(`Required field '${field}' is missing`);
131
+ } else if (frontmatter[field] === "") {
132
+ errors.push(`Required field '${field}' is empty`);
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### The Guard Clause Pattern: `!= null && !== ''`
138
+
139
+ When validating a value (checking if it's in an enum, matches a regex, etc.), guard against
140
+ both missing and empty values to prevent spurious "invalid value" errors:
141
+
142
+ ```javascript
143
+ // RIGHT: Guard against missing and empty before validating
144
+ if (value != null && value !== "" && !VALID_VALUES.includes(value)) {
145
+ errors.push(`Invalid ${fieldName}: '${value}'`);
146
+ }
147
+
148
+ // WRONG: Missing guard allows empty string to be flagged as "invalid"
149
+ if (!VALID_VALUES.includes(value)) {
150
+ // Empty string "" will produce: "Invalid fieldName: ''"
151
+ // Should be "fieldName is empty" instead!
152
+ errors.push(`Invalid ${fieldName}: '${value}'`);
153
+ }
154
+ ```
155
+
156
+ ## Type Coercion Considerations
157
+
158
+ ### When to Use `String()`
159
+
160
+ YAML parsers may return non-string types for values that look like numbers or dates:
161
+
162
+ ```yaml
163
+ version: 1.0.0 # Parsed as number 1 (decimal truncated!)
164
+ created: 2026-01-30 # Parsed as Date object in some parsers
165
+ ```
166
+
167
+ Use `String()` to safely coerce before string operations:
168
+
169
+ ```javascript
170
+ // RIGHT: Coerce before regex matching
171
+ if (frontmatter.version != null && frontmatter.version !== "") {
172
+ const versionStr = String(frontmatter.version);
173
+ if (!versionStr.match(/^\d+\.\d+\.\d+$/)) {
174
+ warnings.push(`Version '${versionStr}' should be semver format`);
175
+ }
176
+ }
177
+
178
+ // WRONG: Assumes string type
179
+ if (frontmatter.version != null && frontmatter.version !== "") {
180
+ if (!frontmatter.version.match(/^\d+\.\d+\.\d+$/)) {
181
+ // TypeError if version is a number!
182
+ warnings.push(`Invalid version format`);
183
+ }
184
+ }
185
+ ```
186
+
187
+ ### Edge Cases with Type Coercion
188
+
189
+ | Input Value | `String(value)` Result | Notes |
190
+ | ----------- | ---------------------- | ----------------------------- |
191
+ | `1.0` | `"1"` | Decimal .0 is lost! |
192
+ | `null` | `"null"` | Check before coercing |
193
+ | `undefined` | `"undefined"` | Check before coercing |
194
+ | `[1, 2]` | `"1,2"` | Array becomes comma-separated |
195
+ | `{a: 1}` | `"[object Object]"` | Objects need special handling |
196
+ | `true` | `"true"` | Boolean to string |
197
+ | `0` | `"0"` | Zero is preserved |
198
+
199
+ **Best Practice**: Always check for `null`/empty before coercing:
200
+
201
+ ```javascript
202
+ if (value != null && value !== "") {
203
+ const strValue = String(value);
204
+ // Now safe to validate strValue
205
+ }
206
+ ```
207
+
208
+ ## The Duplicate Warning Problem
209
+
210
+ ### Anti-Pattern: Overlapping Validation Checks
211
+
212
+ ```javascript
213
+ // BAD: Two separate if statements can both trigger for the same issue
214
+ function validateField(value, fieldName) {
215
+ const warnings = [];
216
+
217
+ // Check 1: Is the value invalid according to enum?
218
+ if (!VALID_VALUES.includes(value)) {
219
+ warnings.push(`Invalid ${fieldName}: '${value}'`);
220
+ }
221
+
222
+ // Check 2: Is the value missing or empty?
223
+ if (value == null || value === "") {
224
+ warnings.push(`Missing ${fieldName}`);
225
+ }
226
+
227
+ return warnings;
228
+ }
229
+
230
+ // When called with empty string:
231
+ validateField("", "complexity.level");
232
+ // Returns TWO warnings:
233
+ // - "Invalid complexity.level: ''" (because '' is not in VALID_VALUES)
234
+ // - "Missing complexity.level" (because '' === "")
235
+ ```
236
+
237
+ ### Correct Pattern: Mutually Exclusive Checks
238
+
239
+ ```javascript
240
+ // GOOD: Ordered checks ensure exactly one warning
241
+ function validateField(value, fieldName) {
242
+ const warnings = [];
243
+
244
+ // Check 1: Is the value missing?
245
+ if (value == null) {
246
+ warnings.push(`Missing ${fieldName}`);
247
+ }
248
+ // Check 2: Is the value empty string?
249
+ else if (value === "") {
250
+ warnings.push(`Empty ${fieldName}`);
251
+ }
252
+ // Check 3: Is the value invalid? (Only check if value is present and non-empty)
253
+ else if (!VALID_VALUES.includes(value)) {
254
+ warnings.push(`Invalid ${fieldName}: '${value}'`);
255
+ }
256
+
257
+ return warnings;
258
+ }
259
+
260
+ // When called with empty string:
261
+ validateField("", "complexity.level");
262
+ // Returns exactly ONE warning:
263
+ // - "Empty complexity.level"
264
+ ```
265
+
266
+ ## Real-World Example: Skill File Validation
267
+
268
+ This pattern is implemented in [validate-skills.js](../../scripts/validate-skills.js):
269
+
270
+ ```javascript
271
+ // Validate complexity level - exclude empty values from enum check
272
+ if (
273
+ frontmatter.complexity != null &&
274
+ frontmatter.complexity.level != null &&
275
+ frontmatter.complexity.level !== "" && // <-- Key: exclude empty strings
276
+ !VALID_COMPLEXITY_LEVELS.includes(frontmatter.complexity.level)
277
+ ) {
278
+ warnings.push(
279
+ new ValidationError(
280
+ skillFile.relativePath,
281
+ "complexity.level",
282
+ `Invalid complexity level '${frontmatter.complexity.level}'. Valid: ${VALID_COMPLEXITY_LEVELS.join(", ")}`
283
+ )
284
+ );
285
+ }
286
+
287
+ // Later, check for missing/empty (mutually exclusive with above)
288
+ if (frontmatter.complexity == null || frontmatter.complexity.level == null) {
289
+ warnings.push(
290
+ new ValidationError(
291
+ skillFile.relativePath,
292
+ "complexity.level",
293
+ `Missing 'complexity.level' - will show '?' in Complexity column of skills index`
294
+ )
295
+ );
296
+ } else if (frontmatter.complexity.level === "") {
297
+ warnings.push(
298
+ new ValidationError(
299
+ skillFile.relativePath,
300
+ "complexity.level",
301
+ `Empty 'complexity.level' - will show '?' in Complexity column of skills index`
302
+ )
303
+ );
304
+ }
305
+ ```
306
+
307
+ ## Integration Testing for Warning Counts
308
+
309
+ Always write integration tests that verify the exact number of warnings produced:
310
+
311
+ ```javascript
312
+ // GOOD: Integration test that verifies no duplicate warnings
313
+ describe("empty string complexity.level produces exactly one warning", () => {
314
+ test("should produce exactly 1 warning (no duplicate from invalid enum check)", () => {
315
+ const content = createValidFrontmatter({
316
+ complexity: { level: "" }
317
+ });
318
+ const skillFile = createMockSkillFile(tempDir, content);
319
+
320
+ const result = validateSkill(skillFile);
321
+
322
+ // Filter warnings for the specific field
323
+ const complexityWarnings = result.warnings.filter((w) => w.field === "complexity.level");
324
+
325
+ // Key assertion: exactly ONE warning, not two
326
+ expect(complexityWarnings).toHaveLength(1);
327
+ expect(complexityWarnings[0].message).toContain("Empty 'complexity.level'");
328
+ });
329
+ });
330
+ ```
331
+
332
+ ## Validation Check Ordering Rules
333
+
334
+ 1. **Null/undefined first** - the most fundamental check (is the value present?)
335
+ 1. **Empty string second** - present but semantically empty
336
+ 1. **Type checks third** - is it the right type (array, object, string)?
337
+ 1. **Format/enum checks last** - only check value validity if value is present and correct type
338
+
339
+ ```javascript
340
+ // Optimal ordering for comprehensive validation
341
+ function validateOptionalField(value, fieldName, validValues) {
342
+ // 1. Missing check
343
+ if (value == null) {
344
+ return [{ type: "warning", message: `Missing ${fieldName}` }];
345
+ }
346
+
347
+ // 2. Empty string check
348
+ if (value === "") {
349
+ return [{ type: "warning", message: `Empty ${fieldName}` }];
350
+ }
351
+
352
+ // 3. Type check (if applicable)
353
+ if (typeof value !== "string") {
354
+ return [{ type: "warning", message: `${fieldName} must be a string` }];
355
+ }
356
+
357
+ // 4. Enum/format check (only reached if all above pass)
358
+ if (!validValues.includes(value)) {
359
+ return [{ type: "warning", message: `Invalid ${fieldName}: '${value}'` }];
360
+ }
361
+
362
+ return []; // Valid
363
+ }
364
+ ```
365
+
366
+ ## Common Mistakes Checklist
367
+
368
+ Before committing validation code, verify:
369
+
370
+ - [ ] Empty strings don't trigger both "invalid enum" and "empty field" warnings
371
+ - [ ] Null/undefined values don't trigger both "invalid enum" and "missing field" warnings
372
+ - [ ] Integration tests verify exactly one warning per field per condition
373
+ - [ ] Validation checks use else-if chains or early returns to ensure mutual exclusivity
374
+ - [ ] Filter by field name when counting warnings in tests (to isolate the field under test)
375
+
376
+ ## Troubleshooting
377
+
378
+ ### Duplicate Warnings Appearing in Output
379
+
380
+ **Symptom**: A field produces two similar warnings (e.g., both "Missing X" and "Invalid X: ''")
381
+
382
+ **Diagnosis**:
383
+
384
+ 1. Check if the enum validation excludes empty/null values with explicit checks
385
+ 1. Verify else-if chains are used instead of separate if statements
386
+ 1. Run integration tests that filter by field name and verify warning count
387
+
388
+ **Solution**: Ensure enum validation includes `value !== ''` in its guard clause:
389
+
390
+ ```javascript
391
+ // Add empty string exclusion to enum check
392
+ if (value != null && value !== "" && !VALID_VALUES.includes(value)) {
393
+ // Only one warning path
394
+ }
395
+ ```
396
+
397
+ ### Whitespace-Only Strings Triggering Wrong Warning
398
+
399
+ **Symptom**: Input like `" "` produces "Invalid enum" instead of "Empty field"
400
+
401
+ **Explanation**: Current validation does not trim whitespace, so `" "` is treated as a
402
+ non-empty string that fails enum validation. This is intentional - if whitespace-only should
403
+ be treated as empty, add explicit `.trim()` checks.
404
+
405
+ ### Test Counting More Warnings Than Expected
406
+
407
+ **Symptom**: Test expects 1 warning but gets 2+
408
+
409
+ **Diagnosis**:
410
+
411
+ 1. Filter warnings by field name: `result.warnings.filter(w => w.field === "fieldName")`
412
+ 1. Log all warnings to see what's being produced
413
+ 1. Check if both enum validation and missing/empty checks are triggering
414
+
415
+ ## See Also
416
+
417
+ - [JavaScript Code Quality](javascript-code-quality.md) - General JavaScript best practices
418
+ - [Comprehensive Test Coverage](../testing/comprehensive-test-coverage.md) - Test coverage requirements
@@ -63,6 +63,9 @@ This document defines the structure, schema, and tooling for storing code patter
63
63
  │ │ ├── powershell-best-practices.md
64
64
  │ │ └── shell-patterns.md
65
65
  │ │
66
+ │ ├── github-actions/ # GitHub Actions workflow patterns
67
+ │ │ └── workflow-consistency.md
68
+ │ │
66
69
  │ └── documentation/ # Documentation and code comments
67
70
  │ └── documentation-updates.md
68
71
  ```
@@ -78,7 +81,7 @@ Every skill file MUST include the following YAML frontmatter:
78
81
  # Required Fields
79
82
  title: "Human-readable skill title"
80
83
  id: "unique-kebab-case-identifier"
81
- category: "performance|testing|solid|messaging|unity|concurrency|architecture|error-handling|code-generation|documentation|scripting"
84
+ category: "performance|testing|solid|messaging|unity|concurrency|architecture|error-handling|code-generation|documentation|scripting|github-actions"
82
85
  version: "1.0.0"
83
86
  created: "2026-01-21"
84
87
  updated: "2026-01-21"
@@ -0,0 +1,243 @@
1
+ ---
2
+ id: test-code-quality
3
+ title: Test Code Quality and Accuracy
4
+ description: Best practices for test documentation accuracy, linter directive placement, and user-facing message consistency
5
+ category: testing
6
+ version: "1.0.0"
7
+ created: "2026-01-30"
8
+ updated: "2026-01-30"
9
+ status: stable
10
+
11
+ source:
12
+ repository: "wallstop/DxMessaging"
13
+ files:
14
+ - path: "scripts/__tests__/validate-skills-tags.test.js"
15
+ - path: "scripts/__tests__/validate-skills-optional-fields.test.js"
16
+ url: "https://github.com/wallstop/DxMessaging"
17
+
18
+ tags:
19
+ - testing
20
+ - documentation
21
+ - linting
22
+ - code-quality
23
+ - javascript
24
+ related:
25
+ - comprehensive-test-coverage
26
+ - script-test-coverage
27
+ complexity:
28
+ level: intermediate
29
+ reasoning: "Requires understanding of JavaScript truthiness and test organization principles"
30
+ impact:
31
+ performance:
32
+ rating: medium
33
+ description: Prevents misleading documentation and ensures test accuracy
34
+ maintainability:
35
+ rating: high
36
+ description: "Accurate test documentation improves codebase understanding"
37
+ ---
38
+
39
+ # Test Code Quality and Accuracy
40
+
41
+ ## Overview
42
+
43
+ This skill covers best practices for writing accurate test documentation, proper linter directive placement, and ensuring consistency between user-facing messages and actual UI/output.
44
+
45
+ ## Solution
46
+
47
+ ## Linter Directive Placement
48
+
49
+ ### The Problem
50
+
51
+ ESLint disable directives must be placed correctly to work:
52
+
53
+ ```javascript
54
+ // WRONG: Directive is NOT immediately before the line it suppresses
55
+ // eslint-disable-next-line unicorn/no-new-array
56
+ const frontmatter = {
57
+ title: "Sample",
58
+ tags: new Array("testing") // This line is NOT suppressed!
59
+ };
60
+
61
+ // CORRECT: Directive is immediately before the target line
62
+ const frontmatter = {
63
+ title: "Sample",
64
+ // eslint-disable-next-line unicorn/no-new-array
65
+ tags: new Array("testing") // This line IS suppressed
66
+ };
67
+ ```
68
+
69
+ ### Best Practices
70
+
71
+ 1. **`eslint-disable-next-line` suppresses exactly the next line** - not code several lines later
72
+ 1. **Verify the linter is actually configured** - don't add directives for linters that aren't used
73
+ 1. **Check `package.json` and config files** - look for `.eslintrc*`, `eslint.config.*`, or `eslint` in `devDependencies`
74
+ 1. **Remove unnecessary directives** - if no linter is configured, the directive is dead code
75
+
76
+ ## Test Documentation Accuracy
77
+
78
+ ### The Problem
79
+
80
+ Test describe blocks and file headers must accurately describe what tests they contain.
81
+
82
+ ```javascript
83
+ // WRONG: Header says "falsy/undefined" but tests wrong-type values
84
+ /**
85
+ * These tests validate:
86
+ * - Missing tags (falsy/undefined) // Inaccurate!
87
+ */
88
+
89
+ describe("missing tags field", () => {
90
+ test("should warn when tags is empty string", () => {
91
+ // WRONG PLACE
92
+ // Empty string is NOT "missing" - it's a wrong type
93
+ });
94
+ });
95
+
96
+ // CORRECT: Accurate categorization
97
+ /**
98
+ * These tests validate:
99
+ * - Missing tags (undefined/null)
100
+ * - Wrong type for tags (string, number, boolean)
101
+ */
102
+
103
+ describe("missing tags field", () => {
104
+ test("should warn when tags is undefined", () => {
105
+ /* ... */
106
+ });
107
+ test("should warn when tags is null", () => {
108
+ /* ... */
109
+ });
110
+ });
111
+
112
+ describe("wrong type for tags", () => {
113
+ test("should warn when tags is an empty string", () => {
114
+ /* ... */
115
+ });
116
+ test("should warn when tags is a zero", () => {
117
+ /* ... */
118
+ });
119
+ test("should warn when tags is boolean false", () => {
120
+ /* ... */
121
+ });
122
+ });
123
+ ```
124
+
125
+ ### JavaScript Truthiness Reference
126
+
127
+ Understanding JavaScript's truthy/falsy values prevents misclassification:
128
+
129
+ | Value | Truthy/Falsy | Category | Correct Test Location |
130
+ | ------------------- | ------------ | -------------------- | ------------------------ |
131
+ | `undefined` | Falsy | Missing | "missing field" describe |
132
+ | `null` | Falsy | Missing | "missing field" describe |
133
+ | `""` (empty string) | Falsy | Wrong type | "wrong type" describe |
134
+ | `0` | Falsy | Wrong type | "wrong type" describe |
135
+ | `false` | Falsy | Wrong type | "wrong type" describe |
136
+ | `[]` (empty array) | **Truthy** | Correct type (empty) | "empty array" describe |
137
+ | `{}` (empty object) | **Truthy** | Wrong type | "wrong type" describe |
138
+
139
+ ### Key Distinction
140
+
141
+ - **Missing**: Value is `undefined` or `null` - the field doesn't exist or is explicitly null
142
+ - **Wrong type**: Value exists but is the wrong JavaScript type (string instead of array, etc.)
143
+ - **Empty**: Value exists with correct type but has no content (empty array, empty string)
144
+
145
+ ## User-Facing Message Consistency
146
+
147
+ ### The Problem
148
+
149
+ Warning messages and error messages displayed to users must match actual UI/output terminology.
150
+
151
+ ```javascript
152
+ // WRONG: Message says "Difficulty column" but actual table header is "Complexity"
153
+ warnings.push(`Missing 'complexity.level' - will show '?' in Difficulty column`);
154
+
155
+ // CORRECT: Message matches actual column name
156
+ warnings.push(`Missing 'complexity.level' - will show '?' in Complexity column`);
157
+ ```
158
+
159
+ ### Best Practices
160
+
161
+ 1. **Verify against source of truth** - read the actual output generation code to confirm terminology
162
+ 1. **Use grep/search to find actual column names** - e.g., search for table header generation
163
+ 1. **Keep messages synchronized** - when UI changes, update all related messages
164
+ 1. **Add SYNC notes** - reference the source of truth in comments
165
+
166
+ Example:
167
+
168
+ ```javascript
169
+ // SYNC: Column names must match table headers in generate-skills-index.js
170
+ // Actual headers: | Skill | Lines | Complexity | Status | Performance | Tags |
171
+ `Missing 'complexity.level' - will show '?' in Complexity column of skills index`;
172
+ ```
173
+
174
+ ## Test Coverage for Message Content
175
+
176
+ ### The Problem
177
+
178
+ When user-facing messages are changed, tests should verify the message content is correct.
179
+
180
+ ### Best Practice
181
+
182
+ Add tests that check message content matches actual terminology:
183
+
184
+ ```javascript
185
+ describe("warning message content", () => {
186
+ test("should reference correct Complexity column name", () => {
187
+ const warnings = validateComplexityLevel(frontmatter, testPath);
188
+
189
+ expect(warnings[0].message).toContain("Complexity column");
190
+ // Ensures we don't accidentally say "Difficulty column"
191
+ });
192
+
193
+ test("should reference correct Performance column name", () => {
194
+ const warnings = validatePerformanceRating(frontmatter, testPath);
195
+
196
+ expect(warnings[0].message).toContain("Performance column");
197
+ // Ensures we don't accidentally say "Priority column"
198
+ });
199
+ });
200
+ ```
201
+
202
+ ## Bidirectional SYNC Notes
203
+
204
+ ### The Problem
205
+
206
+ SYNC notes only work when they exist in both directions.
207
+
208
+ ```javascript
209
+ // FILE A: validate-skills.js
210
+ // SYNC: Keep logic in sync with validate-skills-tags.test.js validateTags()
211
+ if (frontmatter.tags === undefined || frontmatter.tags === null) {
212
+
213
+ // FILE B: validate-skills-tags.test.js
214
+ /**
215
+ * SYNC: Keep logic in sync with validate-skills.js validateSkill() tags validation block
216
+ */
217
+ function validateTags(frontmatter, relativePath) {
218
+ ```
219
+
220
+ ### Requirements
221
+
222
+ 1. **Always bidirectional** - when A references B, B must reference A
223
+ 1. **Reference function/block names** - not line numbers (which become stale)
224
+ 1. **Be specific** - identify the exact function or logical block
225
+ 1. **Verify references exist** - confirm the target exists before adding a SYNC note
226
+
227
+ ## Pre-Commit Checklist
228
+
229
+ Before committing test code, verify:
230
+
231
+ - [ ] Linter directives are immediately before the line they suppress
232
+ - [ ] Linter is actually configured (check `package.json`, `.eslintrc*`, etc.)
233
+ - [ ] `describe()` block names accurately reflect the tests they contain
234
+ - [ ] File header comments accurately describe test categories
235
+ - [ ] "Missing" vs "wrong type" vs "empty" tests are correctly categorized
236
+ - [ ] User-facing messages match actual UI/output terminology
237
+ - [ ] Message content is tested if it references specific column names or labels
238
+ - [ ] SYNC notes exist in both directions
239
+
240
+ ## See Also
241
+
242
+ - [Comprehensive Test Coverage](comprehensive-test-coverage.md)
243
+ - [Script Test Coverage](script-test-coverage.md)