microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/collections-overview-docs

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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