microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/add-pester-code-coverage

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/tests/linting/Validate-MarkdownFrontmatter.Tests.ps1

949lines · modecode

1#Requires -Modules Pester
2
3BeforeAll {
4 $scriptPath = Join-Path $PSScriptRoot '../../linting/Validate-MarkdownFrontmatter.ps1'
5 . $scriptPath
6 $mockPath = Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1'
7 Import-Module $mockPath -Force
8 $script:SchemaDir = Join-Path $PSScriptRoot '../../linting/schemas'
9 $script:FixtureDir = Join-Path $PSScriptRoot '../Fixtures/Frontmatter'
10 $script:RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path
11}
12
13#region ConvertFrom-YamlFrontmatter Tests
14
15Describe 'ConvertFrom-YamlFrontmatter' -Tag 'Unit' {
16 Context 'Valid YAML input' {
17 It 'Parses single key-value pair' {
18 $content = @"
19---
20description: Test description
21---
22"@
23 $result = ConvertFrom-YamlFrontmatter -Content $content
24 $result.Frontmatter.description | Should -Be 'Test description'
25 }
26
27 It 'Parses multiple key-value pairs' {
28 $content = @"
29---
30title: Test Title
31description: Test description
32ms.date: 2025-01-16
33---
34"@
35 $result = ConvertFrom-YamlFrontmatter -Content $content
36 $result.Frontmatter.title | Should -Be 'Test Title'
37 $result.Frontmatter.description | Should -Be 'Test description'
38 $result.Frontmatter.'ms.date' | Should -Be '2025-01-16'
39 }
40
41 It 'Handles quoted values' {
42 $content = @"
43---
44description: "Quoted value with: colon"
45---
46"@
47 $result = ConvertFrom-YamlFrontmatter -Content $content
48 $result.Frontmatter.description | Should -Be 'Quoted value with: colon'
49 }
50
51 It 'Handles single-quoted values' {
52 $content = @"
53---
54description: 'Single quoted'
55---
56"@
57 $result = ConvertFrom-YamlFrontmatter -Content $content
58 $result.Frontmatter.description | Should -Be 'Single quoted'
59 }
60 }
61
62 Context 'Empty or null input' {
63 It 'Returns null for empty string' {
64 $result = ConvertFrom-YamlFrontmatter -Content ''
65 $result | Should -BeNullOrEmpty
66 }
67
68 It 'Returns null for content without frontmatter delimiters' {
69 $result = ConvertFrom-YamlFrontmatter -Content 'Just some text'
70 $result | Should -BeNullOrEmpty
71 }
72
73 It 'Returns null for whitespace only' {
74 $result = ConvertFrom-YamlFrontmatter -Content ' '
75 $result | Should -BeNullOrEmpty
76 }
77 }
78
79 Context 'Edge cases' {
80 It 'Handles values with colons' {
81 $content = @"
82---
83time: "10:30:00"
84---
85"@
86 $result = ConvertFrom-YamlFrontmatter -Content $content
87 $result.Frontmatter.time | Should -Be '10:30:00'
88 }
89
90 It 'Preserves leading and trailing whitespace in values' {
91 $content = @"
92---
93description: " spaced "
94---
95"@
96 $result = ConvertFrom-YamlFrontmatter -Content $content
97 $result.Frontmatter.description | Should -Be ' spaced '
98 }
99
100 It 'Skips comment lines' {
101 $content = @"
102---
103title: Valid
104# This is a comment
105description: Also valid
106---
107"@
108 $result = ConvertFrom-YamlFrontmatter -Content $content
109 $result.Frontmatter.title | Should -Be 'Valid'
110 $result.Frontmatter.description | Should -Be 'Also valid'
111 }
112
113 It 'Returns FrontmatterEndIndex indicating where frontmatter ends' {
114 $content = @"
115---
116title: Test
117---
118# Body content
119"@
120 $result = ConvertFrom-YamlFrontmatter -Content $content
121 $result.FrontmatterEndIndex | Should -Be 3
122 }
123
124 It 'Falls back to raw value when JSON array parsing fails' {
125 # Malformed JSON array with trailing comma should fall back to raw string
126 $content = @"
127---
128tags: [a, b,]
129---
130"@
131 $result = ConvertFrom-YamlFrontmatter -Content $content
132 # When ConvertFrom-Json fails, the raw value is preserved
133 $result.Frontmatter.tags | Should -Be '[a, b,]'
134 }
135 }
136}
137
138#endregion
139
140#region Get-MarkdownFrontmatter Tests
141
142Describe 'Get-MarkdownFrontmatter' -Tag 'Unit' {
143 Context 'Valid frontmatter extraction' {
144 It 'Extracts frontmatter from Content parameter' {
145 $content = @"
146---
147description: Test description
148---
149
150# Content
151"@
152 $result = Get-MarkdownFrontmatter -Content $content
153 $result.Frontmatter.description | Should -Be 'Test description'
154 }
155
156 It 'Returns correct end index' {
157 $content = @"
158---
159title: Test
160---
161
162Body
163"@
164 $result = Get-MarkdownFrontmatter -Content $content
165 $result.FrontmatterEndIndex | Should -BeGreaterThan 0
166 }
167
168 It 'Extracts from fixture file' {
169 $fixturePath = Join-Path $script:FixtureDir 'valid-docs.md'
170 $result = Get-MarkdownFrontmatter -FilePath $fixturePath
171 $result.Frontmatter | Should -Not -BeNullOrEmpty
172 }
173 }
174
175 Context 'Missing frontmatter' {
176 It 'Returns null frontmatter when no delimiters' {
177 $content = '# Just a heading'
178 $result = Get-MarkdownFrontmatter -Content $content
179 $result.Frontmatter | Should -BeNullOrEmpty
180 }
181
182 It 'Returns null for missing-frontmatter fixture' {
183 $fixturePath = Join-Path $script:FixtureDir 'missing-frontmatter.md'
184 $result = Get-MarkdownFrontmatter -FilePath $fixturePath
185 $result.Frontmatter | Should -BeNullOrEmpty
186 }
187 }
188
189 Context 'Empty frontmatter' {
190 It 'Handles empty frontmatter block' {
191 $content = @"
192---
193---
194
195Content
196"@
197 $result = Get-MarkdownFrontmatter -Content $content
198 $result | Should -Not -BeNullOrEmpty
199 }
200 }
201
202 Context 'File path parameter' {
203 It 'Reads from file path' {
204 $fixturePath = Join-Path $script:FixtureDir 'valid-docs.md'
205 $result = Get-MarkdownFrontmatter -FilePath $fixturePath
206 $result.Frontmatter.title | Should -Be 'Valid Documentation File'
207 }
208 }
209}
210
211#endregion
212
213#region Get-FileTypeInfo Tests
214
215Describe 'Get-FileTypeInfo' -Tag 'Unit' {
216 BeforeAll {
217 # Create temporary test files for FileInfo objects
218 $script:TempTestDir = Join-Path ([System.IO.Path]::GetTempPath()) "FrontmatterTests_$([guid]::NewGuid().ToString('N'))"
219 New-Item -ItemType Directory -Path $script:TempTestDir -Force | Out-Null
220
221 # Create subdirectories to simulate repo structure
222 @(
223 'docs/guide',
224 '.github/instructions',
225 '.github/prompts',
226 '.github/chatmodes',
227 '.devcontainer',
228 '.vscode',
229 'random/path'
230 ) | ForEach-Object {
231 New-Item -ItemType Directory -Path (Join-Path $script:TempTestDir $_) -Force | Out-Null
232 }
233 }
234
235 AfterAll {
236 if (Test-Path $script:TempTestDir) {
237 Remove-Item -Path $script:TempTestDir -Recurse -Force -ErrorAction SilentlyContinue
238 }
239 }
240
241 Context 'Root community files' {
242 It 'Identifies README.md as root community' {
243 $filePath = Join-Path $script:TempTestDir 'README.md'
244 Set-Content -Path $filePath -Value 'test'
245 $file = Get-Item $filePath
246 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
247 $result.GetType().Name | Should -Be 'FileTypeInfo'
248 $result.IsRootCommunityFile | Should -BeTrue
249 }
250
251 It 'Identifies CONTRIBUTING.md as root community' {
252 $filePath = Join-Path $script:TempTestDir 'CONTRIBUTING.md'
253 Set-Content -Path $filePath -Value 'test'
254 $file = Get-Item $filePath
255 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
256 $result.IsRootCommunityFile | Should -BeTrue
257 }
258
259 It 'Identifies CODE_OF_CONDUCT.md as root community' {
260 $filePath = Join-Path $script:TempTestDir 'CODE_OF_CONDUCT.md'
261 Set-Content -Path $filePath -Value 'test'
262 $file = Get-Item $filePath
263 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
264 $result.IsRootCommunityFile | Should -BeTrue
265 }
266
267 It 'Identifies SECURITY.md as root community' {
268 $filePath = Join-Path $script:TempTestDir 'SECURITY.md'
269 Set-Content -Path $filePath -Value 'test'
270 $file = Get-Item $filePath
271 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
272 $result.IsRootCommunityFile | Should -BeTrue
273 }
274
275 It 'Identifies SUPPORT.md as root community' {
276 $filePath = Join-Path $script:TempTestDir 'SUPPORT.md'
277 Set-Content -Path $filePath -Value 'test'
278 $file = Get-Item $filePath
279 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
280 $result.IsRootCommunityFile | Should -BeTrue
281 }
282 }
283
284 Context 'Documentation files' {
285 It 'Identifies docs/**/*.md as docs file' {
286 $filePath = Join-Path $script:TempTestDir 'docs/guide/readme.md'
287 Set-Content -Path $filePath -Value 'test'
288 $file = Get-Item $filePath
289 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
290 $result.IsDocsFile | Should -BeTrue
291 }
292
293 It 'Does not mark root README as docs file' {
294 $filePath = Join-Path $script:TempTestDir 'README.md'
295 Set-Content -Path $filePath -Value 'test'
296 $file = Get-Item $filePath
297 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
298 $result.IsDocsFile | Should -BeFalse
299 }
300 }
301
302 Context 'Instruction files' {
303 It 'Identifies *.instructions.md as instruction file' {
304 $filePath = Join-Path $script:TempTestDir '.github/instructions/test.instructions.md'
305 Set-Content -Path $filePath -Value 'test'
306 $file = Get-Item $filePath
307 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
308 $result.IsInstruction | Should -BeTrue
309 }
310 }
311
312 Context 'Prompt files' {
313 It 'Identifies *.prompt.md as prompt file' {
314 $filePath = Join-Path $script:TempTestDir '.github/prompts/build.prompt.md'
315 Set-Content -Path $filePath -Value 'test'
316 $file = Get-Item $filePath
317 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
318 $result.IsPrompt | Should -BeTrue
319 }
320 }
321
322 Context 'Chatmode files' {
323 It 'Identifies *.chatmode.md as chatmode file' {
324 $filePath = Join-Path $script:TempTestDir '.github/chatmodes/helper.chatmode.md'
325 Set-Content -Path $filePath -Value 'test'
326 $file = Get-Item $filePath
327 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
328 $result.IsChatMode | Should -BeTrue
329 }
330 }
331
332 Context 'Special locations' {
333 It 'Identifies .devcontainer README' {
334 $filePath = Join-Path $script:TempTestDir '.devcontainer/README.md'
335 Set-Content -Path $filePath -Value 'test'
336 $file = Get-Item $filePath
337 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
338 $result.IsDevContainer | Should -BeTrue
339 }
340
341 It 'Identifies .vscode README' {
342 $filePath = Join-Path $script:TempTestDir '.vscode/README.md'
343 Set-Content -Path $filePath -Value 'test'
344 $file = Get-Item $filePath
345 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
346 $result.IsVSCodeReadme | Should -BeTrue
347 }
348 }
349
350 Context 'Unknown file types' {
351 It 'Returns all false for random markdown file' {
352 $filePath = Join-Path $script:TempTestDir 'random/path/file.md'
353 Set-Content -Path $filePath -Value 'test'
354 $file = Get-Item $filePath
355 $result = Get-FileTypeInfo -File $file -RepoRoot $script:TempTestDir
356 $result.IsRootCommunityFile | Should -BeFalse
357 $result.IsDocsFile | Should -BeFalse
358 $result.IsInstruction | Should -BeFalse
359 $result.IsPrompt | Should -BeFalse
360 $result.IsChatMode | Should -BeFalse
361 }
362 }
363}
364
365#endregion
366
367#region Test-MarkdownFooter Tests
368
369Describe 'Test-MarkdownFooter' -Tag 'Unit' {
370 BeforeAll {
371 # Standard Copilot attribution footer
372 $script:ValidFooter = '🤖 Crafted with precision by ✨Copilot following brilliant human instruction, carefully refined by our team of discerning human reviewers.'
373 $script:ValidFooterAlternate = '🤖 Crafted with precision by ✨Copilot following brilliant human instruction, then carefully refined by our team of discerning human reviewers.'
374 }
375
376 Context 'Valid footer patterns' {
377 It 'Returns true for standard Copilot attribution footer' {
378 $content = "# Document`n`nSome content here.`n`n$script:ValidFooter"
379 Test-MarkdownFooter -Content $content | Should -BeTrue
380 }
381
382 It 'Returns true for alternate footer with "then" phrasing' {
383 $content = "# Document`n`nContent.`n`n$script:ValidFooterAlternate"
384 Test-MarkdownFooter -Content $content | Should -BeTrue
385 }
386
387 It 'Returns true when footer has trailing period' {
388 $content = "Content`n`n🤖 Crafted with precision by ✨Copilot following brilliant human instruction, carefully refined by our team of discerning human reviewers."
389 Test-MarkdownFooter -Content $content | Should -BeTrue
390 }
391
392 It 'Returns true when footer has no trailing period' {
393 $content = "Content`n`n🤖 Crafted with precision by ✨Copilot following brilliant human instruction, carefully refined by our team of discerning human reviewers"
394 Test-MarkdownFooter -Content $content | Should -BeTrue
395 }
396 }
397
398 Context 'Missing footer' {
399 It 'Returns false for content without Copilot attribution' {
400 $content = 'Content without the attribution footer'
401 Test-MarkdownFooter -Content $content | Should -BeFalse
402 }
403
404 It 'Returns false for empty content' {
405 Test-MarkdownFooter -Content '' | Should -BeFalse
406 }
407
408 It 'Returns false for partial attribution text' {
409 $content = "Content`n`n🤖 Crafted with precision"
410 Test-MarkdownFooter -Content $content | Should -BeFalse
411 }
412 }
413
414 Context 'Footer variations and normalization' {
415 It 'Handles footer with extra whitespace between words' {
416 $content = "Content`n`n🤖 Crafted with precision by ✨Copilot following brilliant human instruction, carefully refined by our team of discerning human reviewers."
417 Test-MarkdownFooter -Content $content | Should -BeTrue
418 }
419
420 It 'Handles footer after multiple blank lines' {
421 $content = "Content`n`n`n`n$script:ValidFooter"
422 Test-MarkdownFooter -Content $content | Should -BeTrue
423 }
424 }
425}
426
427#endregion
428
429#region Initialize-JsonSchemaValidation Tests
430
431Describe 'Initialize-JsonSchemaValidation' -Tag 'Unit' {
432 Context 'Normal operation' {
433 It 'Returns true when JSON processing is available' {
434 $result = Initialize-JsonSchemaValidation
435 $result | Should -BeTrue
436 }
437
438 It 'Validates JSON can be parsed' {
439 # Function internally tests JSON parsing
440 $result = Initialize-JsonSchemaValidation
441 $result | Should -BeOfType [bool]
442 }
443 }
444}
445
446#endregion
447
448#region Get-SchemaForFile Tests
449
450Describe 'Get-SchemaForFile' -Tag 'Unit' {
451 Context 'Schema mapping' {
452 It 'Returns docs schema for docs files' {
453 $result = Get-SchemaForFile -FilePath 'docs/guide/readme.md' -SchemaDirectory $script:SchemaDir -RepoRoot $script:RepoRoot
454 $result | Should -Match 'docs-frontmatter\.schema\.json'
455 }
456
457 It 'Returns instruction schema for instruction files' {
458 $result = Get-SchemaForFile -FilePath '.github/instructions/test.instructions.md' -SchemaDirectory $script:SchemaDir -RepoRoot $script:RepoRoot
459 $result | Should -Match 'instruction-frontmatter\.schema\.json'
460 }
461
462 It 'Returns prompt schema for prompt files' {
463 $result = Get-SchemaForFile -FilePath '.github/prompts/build.prompt.md' -SchemaDirectory $script:SchemaDir -RepoRoot $script:RepoRoot
464 $result | Should -Match 'prompt-frontmatter\.schema\.json'
465 }
466
467 It 'Returns chatmode schema for chatmode files' {
468 $result = Get-SchemaForFile -FilePath '.github/chatmodes/helper.chatmode.md' -SchemaDirectory $script:SchemaDir -RepoRoot $script:RepoRoot
469 $result | Should -Match 'chatmode-frontmatter\.schema\.json'
470 }
471
472 It 'Returns agent schema for agent files' {
473 $result = Get-SchemaForFile -FilePath '.github/agents/worker.agent.md' -SchemaDirectory $script:SchemaDir -RepoRoot $script:RepoRoot
474 $result | Should -Match 'agent-frontmatter\.schema\.json'
475 }
476
477 It 'Returns root-community schema for root community files' {
478 $result = Get-SchemaForFile -FilePath 'README.md' -SchemaDirectory $script:SchemaDir -RepoRoot $script:RepoRoot
479 $result | Should -Match 'root-community-frontmatter\.schema\.json'
480 }
481
482 It 'Returns base schema for unknown file types' {
483 $result = Get-SchemaForFile -FilePath 'random/file.md' -SchemaDirectory $script:SchemaDir -RepoRoot $script:RepoRoot
484 $result | Should -Match 'base-frontmatter\.schema\.json'
485 }
486 }
487}
488
489#endregion
490
491#region Test-JsonSchemaValidation Tests
492
493Describe 'Test-JsonSchemaValidation' -Tag 'Unit' {
494 BeforeAll {
495 $script:DocsSchemaPath = Join-Path $script:SchemaDir 'docs-frontmatter.schema.json'
496 $script:DocsSchema = Get-Content -Path $script:DocsSchemaPath -Raw | ConvertFrom-Json
497 $script:BaseSchemaPath = Join-Path $script:SchemaDir 'base-frontmatter.schema.json'
498 $script:BaseSchema = Get-Content -Path $script:BaseSchemaPath -Raw | ConvertFrom-Json
499 }
500
501 Context 'Required fields validation' {
502 It 'Fails when required field is missing' {
503 $frontmatter = @{ title = 'Test' }
504 $result = Test-JsonSchemaValidation -Frontmatter $frontmatter -SchemaContent $script:DocsSchema
505 $result.GetType().Name | Should -Be 'SchemaValidationResult'
506 $result.IsValid | Should -BeFalse
507 }
508
509 It 'Passes with all required fields' {
510 $frontmatter = @{
511 title = 'Test'
512 description = 'Valid description'
513 }
514 $result = Test-JsonSchemaValidation -Frontmatter $frontmatter -SchemaContent $script:DocsSchema
515 $result.IsValid | Should -BeTrue
516 }
517 }
518
519 Context 'Pattern validation' {
520 BeforeAll {
521 # Create inline schema since $ref is not resolved by Test-JsonSchemaValidation
522 $script:PatternTestSchema = @{
523 required = @('title', 'description')
524 properties = @{
525 title = @{ type = 'string'; minLength = 1 }
526 description = @{ type = 'string'; minLength = 1 }
527 'ms.date' = @{ type = 'string'; pattern = '^\d{4}-\d{2}-\d{2}$' }
528 }
529 } | ConvertTo-Json -Depth 10 | ConvertFrom-Json
530 }
531
532 It 'Fails for invalid date format' {
533 $frontmatter = @{
534 title = 'Test'
535 description = 'Valid'
536 'ms.date' = '2025/01/16'
537 }
538 $result = Test-JsonSchemaValidation -Frontmatter $frontmatter -SchemaContent $script:PatternTestSchema
539 $result.IsValid | Should -BeFalse
540 }
541
542 It 'Passes for valid date format' {
543 $frontmatter = @{
544 title = 'Test'
545 description = 'Valid'
546 'ms.date' = '2025-01-16'
547 }
548 $result = Test-JsonSchemaValidation -Frontmatter $frontmatter -SchemaContent $script:PatternTestSchema
549 $result.IsValid | Should -BeTrue
550 }
551 }
552
553 Context 'Enum validation' {
554 It 'Fails for invalid ms.topic value' {
555 $frontmatter = @{
556 title = 'Test'
557 description = 'Valid'
558 'ms.topic' = 'invalid-topic-type'
559 }
560 $result = Test-JsonSchemaValidation -Frontmatter $frontmatter -SchemaContent $script:DocsSchema
561 $result.IsValid | Should -BeFalse
562 }
563
564 It 'Passes for valid ms.topic value' {
565 $frontmatter = @{
566 title = 'Test'
567 description = 'Valid'
568 'ms.topic' = 'overview'
569 }
570 $result = Test-JsonSchemaValidation -Frontmatter $frontmatter -SchemaContent $script:DocsSchema
571 $result.IsValid | Should -BeTrue
572 }
573 }
574
575 Context 'Return type structure' {
576 It 'Returns SchemaValidationResult with expected properties' {
577 $frontmatter = @{ description = 'Test' }
578 $result = Test-JsonSchemaValidation -Frontmatter $frontmatter -SchemaContent $script:BaseSchema
579 $result.PSObject.Properties.Name | Should -Contain 'IsValid'
580 $result.PSObject.Properties.Name | Should -Contain 'Errors'
581 $result.PSObject.Properties.Name | Should -Contain 'Warnings'
582 $result.PSObject.Properties.Name | Should -Contain 'SchemaUsed'
583 }
584 }
585}
586
587#endregion
588
589#region Get-ChangedMarkdownFileGroup Tests
590
591Describe 'Get-ChangedMarkdownFileGroup' -Tag 'Unit' {
592 BeforeAll {
593 Save-GitHubEnvironment
594 }
595
596 AfterAll {
597 Restore-GitHubEnvironment
598 }
599
600 Context 'Merge-base succeeds' {
601 BeforeEach {
602 Mock git {
603 $global:LASTEXITCODE = 0
604 return 'abc123def456789'
605 } -ParameterFilter { $args[0] -eq 'merge-base' }
606
607 Mock git {
608 $global:LASTEXITCODE = 0
609 return @('docs/test.md', 'README.md', 'scripts/README.md')
610 } -ParameterFilter { $args[0] -eq 'diff' }
611
612 Mock Test-Path { return $true } -ParameterFilter { $PathType -eq 'Leaf' }
613 }
614
615 It 'Returns changed markdown files' {
616 $result = Get-ChangedMarkdownFileGroup
617 $result | Should -BeOfType [string]
618 $result | Should -Contain 'docs/test.md'
619 $result | Should -Contain 'README.md'
620 }
621
622 It 'Filters to markdown files only' {
623 Mock git {
624 $global:LASTEXITCODE = 0
625 return @('test.md', 'test.ps1', 'test.json')
626 } -ParameterFilter { $args[0] -eq 'diff' }
627
628 $result = Get-ChangedMarkdownFileGroup
629 $result | Should -Contain 'test.md'
630 $result | Should -Not -Contain 'test.ps1'
631 $result | Should -Not -Contain 'test.json'
632 }
633
634 It 'Returns array of strings' {
635 $result = Get-ChangedMarkdownFileGroup
636 $result.Count | Should -BeGreaterOrEqual 0
637 }
638 }
639
640 Context 'Fallback scenarios' {
641 BeforeEach {
642 Mock git {
643 $global:LASTEXITCODE = 128
644 return $null
645 } -ParameterFilter { $args[0] -eq 'merge-base' }
646
647 Mock git {
648 $global:LASTEXITCODE = 0
649 return 'HEAD~1-sha'
650 } -ParameterFilter { $args[0] -eq 'rev-parse' }
651
652 Mock git {
653 $global:LASTEXITCODE = 0
654 return @('fallback.md')
655 } -ParameterFilter { $args[0] -eq 'diff' }
656
657 Mock Test-Path { return $true } -ParameterFilter { $PathType -eq 'Leaf' }
658 }
659
660 It 'Falls back to HEAD~1 when merge-base fails' {
661 $result = Get-ChangedMarkdownFileGroup
662 $result | Should -Contain 'fallback.md'
663 }
664
665 It 'Returns files when fallback succeeds' {
666 $result = Get-ChangedMarkdownFileGroup
667 $result.Count | Should -BeGreaterOrEqual 1
668 }
669 }
670
671 Context 'No changes detected' {
672 BeforeEach {
673 Mock git {
674 $global:LASTEXITCODE = 0
675 return 'abc123'
676 } -ParameterFilter { $args[0] -eq 'merge-base' }
677
678 Mock git {
679 $global:LASTEXITCODE = 0
680 return @()
681 } -ParameterFilter { $args[0] -eq 'diff' }
682 }
683
684 It 'Returns empty array when no changes' {
685 $result = Get-ChangedMarkdownFileGroup
686 $result.Count | Should -Be 0
687 }
688 }
689}
690
691#endregion
692
693#region Test-FrontmatterValidation Integration Tests
694
695Describe 'Test-FrontmatterValidation' -Tag 'Integration' {
696 BeforeAll {
697 Save-GitHubEnvironment
698 $script:TestRepoRoot = Join-Path $TestDrive 'test-repo'
699 }
700
701 BeforeEach {
702 New-Item -Path "$script:TestRepoRoot/docs" -ItemType Directory -Force | Out-Null
703 New-Item -Path "$script:TestRepoRoot/.github/instructions" -ItemType Directory -Force | Out-Null
704 New-Item -Path "$script:TestRepoRoot/scripts/linting/schemas" -ItemType Directory -Force | Out-Null
705
706 Copy-Item -Path "$script:SchemaDir/*" -Destination "$script:TestRepoRoot/scripts/linting/schemas/" -Force
707
708 $schemaMappingSource = Join-Path $script:SchemaDir 'schema-mapping.json'
709 if (Test-Path $schemaMappingSource) {
710 Copy-Item -Path $schemaMappingSource -Destination "$script:TestRepoRoot/scripts/linting/schemas/schema-mapping.json" -Force
711 }
712
713 # Change to test repo root so function detects it as repo root
714 Push-Location $script:TestRepoRoot
715 # Initialize minimal git repo for function's repo root detection
716 git init --quiet
717 }
718
719 AfterEach {
720 Pop-Location
721 }
722
723 AfterAll {
724 Restore-GitHubEnvironment
725 }
726
727 Context 'Valid files pass validation' {
728 BeforeEach {
729 @"
730---
731title: Test Documentation
732description: Valid documentation file
733ms.date: 2025-01-16
734ms.topic: overview
735---
736
737# Test
738
739Content here.
740"@ | Set-Content -Path "$script:TestRepoRoot/docs/test.md" -Encoding UTF8
741 }
742
743 It 'Returns ValidationResult type' {
744 $result = Test-FrontmatterValidation -Files @("$script:TestRepoRoot/docs/test.md")
745 $result.GetType().Name | Should -Be 'ValidationResult'
746 }
747
748 It 'Reports no errors for valid frontmatter' {
749 $result = Test-FrontmatterValidation -Files @("$script:TestRepoRoot/docs/test.md")
750 $result.HasIssues | Should -BeFalse
751 $result.Errors.Count | Should -Be 0
752 }
753 }
754
755 Context 'Missing frontmatter fails' {
756 BeforeEach {
757 @"
758# No Frontmatter
759
760Just content without any YAML.
761"@ | Set-Content -Path "$script:TestRepoRoot/docs/no-frontmatter.md" -Encoding UTF8
762 }
763
764 It 'Reports warning for missing frontmatter' {
765 $result = Test-FrontmatterValidation -Files @("$script:TestRepoRoot/docs/no-frontmatter.md")
766 # Missing frontmatter in docs is a warning, not an error
767 $result.Warnings.Count | Should -BeGreaterThan 0
768 $result.Warnings | Should -Match 'No frontmatter found'
769 }
770 }
771
772 Context 'Empty description fails' {
773 BeforeEach {
774 @"
775---
776title: Has Title
777description: ""
778---
779
780Content
781"@ | Set-Content -Path "$script:TestRepoRoot/docs/empty-desc.md" -Encoding UTF8
782 }
783
784 It 'Reports schema validation warning for empty description' {
785 # Schema validation is soft (advisory) - errors are Write-Warning, not HasIssues
786 $result = Test-FrontmatterValidation -Files @("$script:TestRepoRoot/docs/empty-desc.md")
787 # HasIssues is false because schema errors are advisory warnings
788 $result.HasIssues | Should -BeFalse
789 }
790 }
791
792 Context 'Invalid date format fails' {
793 BeforeEach {
794 # docs-frontmatter.schema.json requires BOTH title AND description
795 @"
796---
797title: Bad Date File
798description: Valid description
799ms.date: 2025/01/16
800---
801
802Content
803"@ | Set-Content -Path "$script:TestRepoRoot/docs/bad-date.md" -Encoding UTF8
804 }
805
806 It 'Reports warning for invalid date format' {
807 # Invalid date format is a warning, not an error
808 $result = Test-FrontmatterValidation -Files @("$script:TestRepoRoot/docs/bad-date.md")
809 $result.HasIssues | Should -BeFalse
810 ($result.Warnings -join "`n") | Should -Match 'Invalid date format'
811 }
812 }
813
814 Context 'Multiple file validation' {
815 BeforeEach {
816 # docs-frontmatter.schema.json requires BOTH title AND description
817 @"
818---
819title: Valid File 1
820description: Valid file 1
821---
822Content
823"@ | Set-Content -Path "$script:TestRepoRoot/docs/valid1.md" -Encoding UTF8
824
825 @"
826---
827title: Valid File 2
828description: Valid file 2
829---
830Content
831"@ | Set-Content -Path "$script:TestRepoRoot/docs/valid2.md" -Encoding UTF8
832 }
833
834 It 'Validates multiple files in directory' {
835 $result = Test-FrontmatterValidation -Paths @("$script:TestRepoRoot/docs")
836 $result.TotalFilesChecked | Should -BeGreaterOrEqual 2
837 }
838 }
839
840 Context 'Result aggregation' {
841 It 'Aggregates errors and warnings in result' {
842 # docs-frontmatter.schema.json requires BOTH title AND description
843 @"
844---
845title: Test File
846description: Valid
847---
848Content
849"@ | Set-Content -Path "$script:TestRepoRoot/docs/test.md" -Encoding UTF8
850
851 $result = Test-FrontmatterValidation -Files @("$script:TestRepoRoot/docs/test.md")
852 $result.PSObject.Properties.Name | Should -Contain 'Errors'
853 $result.PSObject.Properties.Name | Should -Contain 'Warnings'
854 $result.PSObject.Properties.Name | Should -Contain 'HasIssues'
855 $result.PSObject.Properties.Name | Should -Contain 'TotalFilesChecked'
856 }
857 }
858}
859
860#endregion
861
862#region ExcludePaths Filtering Tests
863
864Describe 'ExcludePaths Filtering' -Tag 'Unit' {
865 BeforeAll {
866 # Create test directory structure with files to include and exclude
867 $script:ExcludeTestDir = Join-Path $TestDrive 'exclude-test'
868 New-Item -ItemType Directory -Path "$script:ExcludeTestDir/docs" -Force | Out-Null
869 New-Item -ItemType Directory -Path "$script:ExcludeTestDir/tests/fixtures" -Force | Out-Null
870
871 # Valid file that should be included
872 @"
873---
874title: Include This
875description: File that should be validated
876---
877Content
878"@ | Set-Content -Path "$script:ExcludeTestDir/docs/include.md" -Encoding UTF8
879
880 # File in tests directory that should be excluded
881 @"
882---
883title: Exclude This
884description: File in tests folder
885---
886Content
887"@ | Set-Content -Path "$script:ExcludeTestDir/tests/fixtures/exclude.md" -Encoding UTF8
888 }
889
890 Context 'Excludes files matching single pattern' {
891 It 'Excludes files matching pattern with wildcard prefix' {
892 # Use wildcard prefix since ExcludePaths computes relative path from repo root
893 # For files outside repo, the full path is used, so we match with *tests*
894 $result = Test-FrontmatterValidation -Paths @($script:ExcludeTestDir) -ExcludePaths @('*tests*')
895 # Should only check docs/include.md, not tests/fixtures/exclude.md
896 $result.TotalFilesChecked | Should -Be 1
897 }
898 }
899
900 Context 'Excludes files matching multiple patterns' {
901 BeforeAll {
902 # Add another directory to exclude
903 New-Item -ItemType Directory -Path "$script:ExcludeTestDir/vendor" -Force | Out-Null
904 @"
905---
906title: Vendor File
907description: Third party content
908---
909Content
910"@ | Set-Content -Path "$script:ExcludeTestDir/vendor/third-party.md" -Encoding UTF8
911 }
912
913 It 'Excludes files matching multiple patterns' {
914 $result = Test-FrontmatterValidation -Paths @($script:ExcludeTestDir) -ExcludePaths @('*tests*', '*vendor*')
915 # Should only check docs/include.md
916 $result.TotalFilesChecked | Should -Be 1
917 }
918 }
919
920 Context 'Processes all files when ExcludePaths is empty' {
921 It 'Validates all markdown files without exclusions' {
922 $result = Test-FrontmatterValidation -Paths @($script:ExcludeTestDir) -ExcludePaths @()
923 # Should check all markdown files (docs + tests + vendor)
924 $result.TotalFilesChecked | Should -BeGreaterOrEqual 2
925 }
926 }
927
928 Context 'Pattern matching behavior' {
929 It 'Matches glob pattern with double asterisk for relative paths' {
930 $relativePath = 'tests/fixtures/exclude.md'
931 $pattern = 'tests/**'
932 $relativePath -like $pattern | Should -BeTrue
933 }
934
935 It 'Does not match non-matching patterns' {
936 $relativePath = 'docs/include.md'
937 $pattern = 'tests/**'
938 $relativePath -like $pattern | Should -BeFalse
939 }
940
941 It 'Matches pattern with single asterisk for file names' {
942 $relativePath = 'docs/README.md'
943 $pattern = 'docs/*.md'
944 $relativePath -like $pattern | Should -BeTrue
945 }
946 }
947}
948
949#endregion
950