microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/a11y-pr1-scripts-validators

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/tests/linting/Invoke-MsDateFreshnessCheck.Tests.ps1

540lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4<#
5.SYNOPSIS
6 Pester tests for Invoke-MsDateFreshnessCheck.ps1 script
7.DESCRIPTION
8 Tests for ms.date frontmatter freshness checking:
9 - File discovery with exclusions
10 - ChangedFilesOnly filtering via mocked git
11 - ms.date parsing (valid, invalid, missing)
12 - Report generation (JSON and markdown)
13 - Integration smoke test with CI annotations
14#>
15
16BeforeAll {
17 $lintingHelpersPath = Join-Path $PSScriptRoot '../../linting/Modules/LintingHelpers.psm1'
18 $ciHelpersPath = Join-Path $PSScriptRoot '../../lib/Modules/CIHelpers.psm1'
19
20 Import-Module $lintingHelpersPath -Force
21 Import-Module $ciHelpersPath -Force
22 Import-Module (Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1') -Force
23 Import-Module powershell-yaml -Force
24
25 . $PSScriptRoot/../../linting/Invoke-MsDateFreshnessCheck.ps1
26 $ErrorActionPreference = 'Continue'
27}
28
29AfterAll {
30 Remove-Module LintingHelpers -Force -ErrorAction SilentlyContinue
31 Remove-Module CIHelpers -Force -ErrorAction SilentlyContinue
32 Remove-Module GitMocks -Force -ErrorAction SilentlyContinue
33}
34
35#region Get-MarkdownFiles Tests
36
37Describe 'Get-MarkdownFiles' -Tag 'Unit' {
38 BeforeAll {
39 Save-CIEnvironment
40 }
41
42 AfterAll {
43 Restore-CIEnvironment
44 }
45
46 BeforeEach {
47 $script:TestDir = Join-Path $TestDrive 'ms-date-test'
48 New-Item -ItemType Directory -Path $script:TestDir -Force | Out-Null
49 Push-Location $script:TestDir
50 }
51
52 AfterEach {
53 Pop-Location
54 Restore-CIEnvironment
55 }
56
57 Context 'File discovery' {
58 BeforeEach {
59 New-Item -ItemType File -Path (Join-Path $script:TestDir 'readme.md') -Force | Out-Null
60 New-Item -ItemType Directory -Path (Join-Path $script:TestDir 'docs') -Force | Out-Null
61 New-Item -ItemType File -Path (Join-Path $script:TestDir 'docs/guide.md') -Force | Out-Null
62 New-Item -ItemType File -Path (Join-Path $script:TestDir 'docs/tutorial.md') -Force | Out-Null
63 }
64
65 It 'Discovers markdown files recursively' {
66 $files = @(Get-MarkdownFiles -SearchPaths @($script:TestDir))
67 $files.Count | Should -BeGreaterOrEqual 3
68 }
69
70 It 'Returns FileInfo objects' {
71 $files = @(Get-MarkdownFiles -SearchPaths @($script:TestDir))
72 $files[0] | Should -BeOfType [System.IO.FileInfo]
73 }
74 }
75
76 Context 'Exclusion patterns' {
77 BeforeEach {
78 Push-Location $script:TestDir
79 New-Item -ItemType Directory -Path 'node_modules' -Force | Out-Null
80 New-Item -ItemType File -Path 'node_modules/package.md' -Force | Out-Null
81 New-Item -ItemType Directory -Path '.git' -Force | Out-Null
82 New-Item -ItemType File -Path '.git/commit.md' -Force | Out-Null
83 New-Item -ItemType Directory -Path 'logs' -Force | Out-Null
84 New-Item -ItemType File -Path 'logs/output.md' -Force | Out-Null
85 New-Item -ItemType Directory -Path '.copilot-tracking' -Force | Out-Null
86 New-Item -ItemType File -Path '.copilot-tracking/notes.md' -Force | Out-Null
87 New-Item -ItemType Directory -Path 'plugins/hve-core' -Force | Out-Null
88 New-Item -ItemType File -Path 'plugins/hve-core/README.md' -Force | Out-Null
89 New-Item -ItemType File -Path 'CHANGELOG.md' -Force | Out-Null
90 New-Item -ItemType File -Path 'valid.md' -Force | Out-Null
91 }
92
93 AfterEach {
94 Pop-Location
95 }
96
97 It 'Excludes node_modules directory' {
98 $files = @(Get-MarkdownFiles -SearchPaths @('.'))
99 $files.Name | Should -Not -Contain 'package.md'
100 }
101
102 It 'Excludes .git directory' {
103 $files = @(Get-MarkdownFiles -SearchPaths @('.'))
104 $files.Name | Should -Not -Contain 'commit.md'
105 }
106
107 It 'Excludes logs directory' {
108 $files = @(Get-MarkdownFiles -SearchPaths @('.'))
109 $files.Name | Should -Not -Contain 'output.md'
110 }
111
112 It 'Excludes .copilot-tracking directory' {
113 $files = @(Get-MarkdownFiles -SearchPaths @('.'))
114 $files.Name | Should -Not -Contain 'notes.md'
115 }
116
117 It 'Excludes plugins directory' {
118 $files = @(Get-MarkdownFiles -SearchPaths @('.'))
119 $files.Name | Should -Not -Contain 'README.md'
120 }
121
122 It 'Excludes CHANGELOG.md' {
123 $files = @(Get-MarkdownFiles -SearchPaths @('.'))
124 $files.Name | Should -Not -Contain 'CHANGELOG.md'
125 }
126
127 It 'Includes non-excluded files' {
128 $files = @(Get-MarkdownFiles -SearchPaths @('.'))
129 $files.Name | Should -Contain 'valid.md'
130 }
131 }
132
133 Context 'Explicit path mode' {
134 BeforeEach {
135 New-Item -ItemType Directory -Path (Join-Path $script:TestDir 'logs') -Force | Out-Null
136 $script:ExplicitFile = Join-Path $script:TestDir 'logs/specific.md'
137 New-Item -ItemType File -Path $script:ExplicitFile -Force | Out-Null
138 }
139
140 It 'Includes excluded directories when path is explicit' {
141 $files = @(Get-MarkdownFiles -SearchPaths @($script:ExplicitFile))
142 $files.FullName | Should -Contain $script:ExplicitFile
143 }
144 }
145
146 Context 'ChangedFilesOnly mode' {
147 BeforeEach {
148 Push-Location $script:TestDir
149 New-Item -ItemType File -Path 'changed.md' -Force | Out-Null
150 New-Item -ItemType File -Path 'unchanged.md' -Force | Out-Null
151
152 Initialize-MockCIEnvironment -Workspace $script:TestDir | Out-Null
153
154 Mock git {
155 $global:LASTEXITCODE = 0
156 return 'abc123'
157 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
158
159 Mock git {
160 $global:LASTEXITCODE = 0
161 return @('changed.md')
162 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
163 }
164
165 AfterEach {
166 Pop-Location
167 }
168
169 It 'Uses Git changed files when ChangedOnly is set' {
170 $files = @(Get-MarkdownFiles -SearchPaths @('.') -ChangedOnly -Base 'origin/main')
171 $files.Count | Should -Be 1
172 $files[0] | Should -Be 'changed.md'
173 }
174
175 It 'Filters out non-existent changed files' {
176 Mock git {
177 $global:LASTEXITCODE = 0
178 return @('missing.md')
179 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
180
181 $files = @(Get-MarkdownFiles -SearchPaths @('.') -ChangedOnly -Base 'origin/main')
182 $files | Should -BeNullOrEmpty
183 }
184 }
185
186 Context 'Multiple paths' {
187 BeforeEach {
188 $script:Path1 = Join-Path $script:TestDir 'dir1'
189 $script:Path2 = Join-Path $script:TestDir 'dir2'
190 New-Item -ItemType Directory -Path $script:Path1 -Force | Out-Null
191 New-Item -ItemType Directory -Path $script:Path2 -Force | Out-Null
192 New-Item -ItemType File -Path (Join-Path $script:Path1 'file1.md') -Force | Out-Null
193 New-Item -ItemType File -Path (Join-Path $script:Path2 'file2.md') -Force | Out-Null
194 }
195
196 It 'Searches multiple paths' {
197 $files = @(Get-MarkdownFiles -SearchPaths @($script:Path1, $script:Path2))
198 $files.Count | Should -Be 2
199 }
200 }
201
202 Context 'Edge cases' {
203 It 'Returns empty array for non-existent path' {
204 $files = @(Get-MarkdownFiles -SearchPaths @('/non-existent-path-xyz-12345') -WarningAction SilentlyContinue)
205 $files | Should -BeNullOrEmpty
206 }
207 }
208}
209
210#endregion
211
212#region Get-MsDateFromFrontmatter Tests
213
214Describe 'Get-MsDateFromFrontmatter' -Tag 'Unit' {
215 BeforeEach {
216 $script:TestFile = Join-Path $TestDrive 'test-frontmatter.md'
217 }
218
219 Context 'Valid ms.date' {
220 It 'Returns DateTime for valid ISO 8601 date' {
221 Set-Content -Path $script:TestFile -Value @'
222---
223title: Test
224ms.date: 2025-06-15
225---
226# Content
227'@
228 $result = Get-MsDateFromFrontmatter -FilePath $script:TestFile
229 $result | Should -BeOfType [DateTime]
230 $result.Year | Should -Be 2025
231 $result.Month | Should -Be 6
232 $result.Day | Should -Be 15
233 }
234
235 It 'Parses ms.date alongside other frontmatter fields' {
236 Set-Content -Path $script:TestFile -Value @'
237---
238title: Example
239description: This is a test
240ms.date: 2024-06-15
241author: tester
242---
243Content
244'@
245 $result = Get-MsDateFromFrontmatter -FilePath $script:TestFile
246 $result | Should -BeOfType [DateTime]
247 $result.ToString('yyyy-MM-dd') | Should -Be '2024-06-15'
248 }
249 }
250
251 Context 'Missing ms.date' {
252 It 'Returns null when ms.date key is absent' {
253 Set-Content -Path $script:TestFile -Value @'
254---
255title: No Date Field
256---
257Content
258'@
259 $result = Get-MsDateFromFrontmatter -FilePath $script:TestFile
260 $result | Should -BeNullOrEmpty
261 }
262 }
263
264 Context 'Invalid ms.date format' {
265 It 'Returns null for wrong date separator format' {
266 Set-Content -Path $script:TestFile -Value @'
267---
268ms.date: 2025/01/01
269---
270Content
271'@
272 $result = Get-MsDateFromFrontmatter -FilePath $script:TestFile
273 $result | Should -BeNullOrEmpty
274 }
275
276 It 'Returns null for non-date string value' {
277 Set-Content -Path $script:TestFile -Value @'
278---
279ms.date: invalid-date
280---
281Content
282'@
283 $result = Get-MsDateFromFrontmatter -FilePath $script:TestFile
284 $result | Should -BeNullOrEmpty
285 }
286 }
287
288 Context 'Malformed frontmatter' {
289 It 'Returns null when frontmatter has no closing delimiter' {
290 Set-Content -Path $script:TestFile -Value @'
291---
292title: Incomplete
293'@
294 $result = Get-MsDateFromFrontmatter -FilePath $script:TestFile
295 $result | Should -BeNullOrEmpty
296 }
297
298 It 'Returns null when file has no frontmatter' {
299 Set-Content -Path $script:TestFile -Value @'
300# Regular Markdown
301No frontmatter here.
302'@
303 $result = Get-MsDateFromFrontmatter -FilePath $script:TestFile
304 $result | Should -BeNullOrEmpty
305 }
306
307 It 'Handles malformed YAML gracefully' {
308 Set-Content -Path $script:TestFile -Value @'
309---
310title: "Unclosed quote
311ms.date: 2025-01-01
312---
313Content
314'@
315 $result = Get-MsDateFromFrontmatter -FilePath $script:TestFile
316 $result | Should -BeNullOrEmpty
317 }
318 }
319
320 Context 'File access errors' {
321 It 'Returns null when file cannot be read' {
322 $result = Get-MsDateFromFrontmatter -FilePath (Join-Path $TestDrive 'nonexistent.md') 3>$null
323 $result | Should -BeNullOrEmpty
324 }
325
326 It 'Emits warning when file cannot be read' {
327 $warnings = @(Get-MsDateFromFrontmatter -FilePath (Join-Path $TestDrive 'nonexistent.md') 3>&1)
328 $warnings | Where-Object { $_ -like '*Error reading file*' } | Should -Not -BeNullOrEmpty
329 }
330 }
331}
332
333#endregion
334#region New-MsDateReport Tests
335
336Describe 'New-MsDateReport' -Tag 'Unit' {
337 BeforeEach {
338 Push-Location $TestDrive
339 $script:Results = @(
340 [PSCustomObject]@{ File = 'docs/fresh.md'; MsDate = '2026-03-01'; AgeDays = 8; IsStale = $false; Threshold = 90 },
341 [PSCustomObject]@{ File = 'docs/stale.md'; MsDate = '2025-11-01'; AgeDays = 128; IsStale = $true; Threshold = 90 },
342 [PSCustomObject]@{ File = 'docs/very-stale.md'; MsDate = '2025-06-01'; AgeDays = 281; IsStale = $true; Threshold = 90 }
343 )
344 }
345
346 AfterEach {
347 Pop-Location
348 }
349
350 Context 'JSON report creation' {
351 It 'Creates msdate-freshness-results.json in logs directory' {
352 New-MsDateReport -Results $script:Results -Threshold 90 -OutputDirectory (Join-Path $TestDrive 'logs')
353 Test-Path (Join-Path $TestDrive 'logs/msdate-freshness-results.json') | Should -BeTrue
354 }
355
356 It 'JSON contains correct schema fields' {
357 New-MsDateReport -Results $script:Results -Threshold 90 -OutputDirectory (Join-Path $TestDrive 'logs')
358 $json = Get-Content (Join-Path $TestDrive 'logs/msdate-freshness-results.json') -Raw | ConvertFrom-Json
359 $json.Count | Should -Be 3
360 $staleItem = $json | Where-Object { $_.File -eq 'docs/stale.md' }
361 $staleItem.AgeDays | Should -Be 128
362 $staleItem.IsStale | Should -BeTrue
363 }
364 }
365
366 Context 'Markdown summary creation' {
367 It 'Creates msdate-summary.md in logs directory' {
368 New-MsDateReport -Results $script:Results -Threshold 90 -OutputDirectory (Join-Path $TestDrive 'logs')
369 Test-Path (Join-Path $TestDrive 'logs/msdate-summary.md') | Should -BeTrue
370 }
371
372 It 'Markdown table lists stale files sorted by AgeDays descending' {
373 New-MsDateReport -Results $script:Results -Threshold 90 -OutputDirectory (Join-Path $TestDrive 'logs')
374 $md = Get-Content (Join-Path $TestDrive 'logs/msdate-summary.md') -Raw
375 $md | Should -Match 'Stale Documentation Files'
376 $veryStaleIndex = $md.IndexOf('docs/very-stale.md')
377 $staleIndex = $md.IndexOf('docs/stale.md')
378 $veryStaleIndex | Should -BeLessThan $staleIndex
379 }
380 }
381
382 Context 'Return values' {
383 It 'Returns object with JsonPath and MarkdownPath properties' {
384 $report = New-MsDateReport -Results $script:Results -Threshold 90 -OutputDirectory (Join-Path $TestDrive 'logs')
385 $report.JsonPath | Should -Not -BeNullOrEmpty
386 $report.MarkdownPath | Should -Not -BeNullOrEmpty
387 }
388
389 It 'Returns StaleCount matching number of stale results' {
390 $report = New-MsDateReport -Results $script:Results -Threshold 90 -OutputDirectory (Join-Path $TestDrive 'logs')
391 $report.StaleCount | Should -Be 2
392 }
393 }
394
395 Context 'All fresh files' {
396 BeforeEach {
397 $script:FreshResults = @(
398 [PSCustomObject]@{ File = 'docs/fresh.md'; MsDate = '2026-03-01'; AgeDays = 8; IsStale = $false; Threshold = 90 }
399 )
400 }
401
402 It 'Shows success message when no stale files' {
403 New-MsDateReport -Results $script:FreshResults -Threshold 90 -OutputDirectory (Join-Path $TestDrive 'logs')
404 $md = Get-Content (Join-Path $TestDrive 'logs/msdate-summary.md') -Raw
405 $md | Should -Match 'All Files Fresh'
406 }
407
408 It 'Does not include stale files table' {
409 New-MsDateReport -Results $script:FreshResults -Threshold 90 -OutputDirectory (Join-Path $TestDrive 'logs')
410 $md = Get-Content (Join-Path $TestDrive 'logs/msdate-summary.md') -Raw
411 $md | Should -Not -Match 'Stale Documentation Files'
412 }
413 }
414}
415
416#endregion
417
418#region Integration Tests
419
420Describe 'Invoke-MsDateFreshnessCheck Integration' -Tag 'Integration' {
421 BeforeAll {
422 Save-CIEnvironment
423 }
424
425 AfterAll {
426 Restore-CIEnvironment
427 }
428
429 BeforeEach {
430 $script:TestDir = Join-Path $TestDrive 'msdate-integration'
431 New-Item -ItemType Directory -Path $script:TestDir -Force | Out-Null
432 Push-Location $script:TestDir
433 New-Item -ItemType Directory -Path 'logs' -Force | Out-Null
434 Initialize-MockCIEnvironment -Workspace $script:TestDir | Out-Null
435 Mock git { return $script:TestDir } -ParameterFilter { $args[0] -eq 'rev-parse' }
436
437 Set-Content (Join-Path $script:TestDir 'fresh.md') @'
438---
439ms.date: 2026-03-01
440title: Fresh Document
441---
442Content
443'@
444
445 Set-Content (Join-Path $script:TestDir 'stale.md') @'
446---
447ms.date: 2025-01-01
448title: Stale Document
449---
450Content
451'@
452
453 Set-Content (Join-Path $script:TestDir 'no-date.md') @'
454---
455title: No Date
456---
457Content
458'@
459 }
460
461 AfterEach {
462 Pop-Location
463 Restore-CIEnvironment
464 }
465
466 Context 'Full workflow' {
467 It 'Processes files and generates reports' {
468 Mock Write-CIAnnotation { }
469 $markdownFiles = @(Get-MarkdownFiles -SearchPaths @($script:TestDir))
470 $results = @()
471 $currentDate = Get-Date
472
473 foreach ($file in $markdownFiles) {
474 $msDate = Get-MsDateFromFrontmatter -FilePath $file
475 if ($null -eq $msDate) { continue }
476 $ageDays = [int](($currentDate - $msDate).TotalDays)
477 $results += [PSCustomObject]@{
478 File = $file.Name
479 MsDate = $msDate.ToString('yyyy-MM-dd')
480 AgeDays = $ageDays
481 IsStale = $ageDays -gt 90
482 Threshold = 90
483 }
484 }
485
486 $results.Count | Should -Be 2
487 $report = New-MsDateReport -Results $results -Threshold 90 -OutputDirectory (Join-Path $script:TestDir 'logs')
488 Test-Path $report.JsonPath | Should -BeTrue
489 Test-Path $report.MarkdownPath | Should -BeTrue
490 $report.StaleCount | Should -BeGreaterThan 0
491 }
492 }
493
494 Context 'CI annotations' {
495 It 'Calls Write-CIAnnotation for stale files' {
496 Mock Write-CIAnnotation { } -Verifiable
497 $markdownFiles = @(Get-MarkdownFiles -SearchPaths @($script:TestDir))
498 $currentDate = Get-Date
499
500 foreach ($file in $markdownFiles) {
501 $relativePath = $file.Name
502 $msDate = Get-MsDateFromFrontmatter -FilePath $file
503 if ($null -eq $msDate) { continue }
504 $ageDays = [int](($currentDate - $msDate).TotalDays)
505 if ($ageDays -gt 90) {
506 Write-CIAnnotation -Message "${relativePath}: ms.date is $ageDays days old (threshold: 90 days)" -Level 'Warning' -File $relativePath
507 }
508 }
509
510 Should -InvokeVerifiable
511 }
512 }
513
514 Context 'Threshold configuration' {
515 It 'Allows custom threshold values' {
516 $threshold = 30
517 $markdownFiles = @(Get-MarkdownFiles -SearchPaths @($script:TestDir))
518 $results = @()
519 $currentDate = Get-Date
520
521 foreach ($file in $markdownFiles) {
522 $msDate = Get-MsDateFromFrontmatter -FilePath $file
523 if ($null -eq $msDate) { continue }
524 $ageDays = [int](($currentDate - $msDate).TotalDays)
525 $results += [PSCustomObject]@{
526 File = $file.Name
527 MsDate = $msDate.ToString('yyyy-MM-dd')
528 AgeDays = $ageDays
529 IsStale = $ageDays -gt $threshold
530 Threshold = $threshold
531 }
532 }
533
534 $staleFiles = @($results | Where-Object { $_.IsStale })
535 $staleFiles.Count | Should -BeGreaterThan 0
536 }
537 }
538}
539
540#endregion
541