microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fix/1232-recursive-plugin

Branches

Tags

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

Clone

HTTPS

Download ZIP

.github/skills/shared/pr-reference/tests/generate.Tests.ps1

511lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4
5BeforeAll {
6 . (Join-Path -Path $PSScriptRoot -ChildPath '../scripts/generate.ps1')
7}
8
9Describe 'Test-GitAvailability' {
10 It 'Does not throw when git is available' {
11 # This test assumes git is installed in the test environment
12 { Test-GitAvailability } | Should -Not -Throw
13 }
14
15 It 'Should throw when git is not available' {
16 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'git' }
17 { Test-GitAvailability } | Should -Throw '*Git is required*'
18 }
19}
20
21Describe 'New-PrDirectory' {
22 BeforeAll {
23 $script:tempRepo = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
24 New-Item -ItemType Directory -Path $script:tempRepo -Force | Out-Null
25 }
26
27 AfterAll {
28 Remove-Item -Path $script:tempRepo -Recurse -Force -ErrorAction SilentlyContinue
29 }
30
31 It 'Creates parent directory for the output file' {
32 $outputFile = Join-Path $script:tempRepo '.copilot-tracking/pr/pr-reference.xml'
33 $result = New-PrDirectory -OutputFilePath $outputFile
34 $result | Should -Not -BeNullOrEmpty
35 Test-Path -Path $result -PathType Container | Should -BeTrue
36 $result | Should -Match '\.copilot-tracking[\\/]pr$'
37 }
38
39 It 'Returns existing directory without error' {
40 $outputFile = Join-Path $script:tempRepo '.copilot-tracking/pr/pr-reference.xml'
41 $firstCall = New-PrDirectory -OutputFilePath $outputFile
42 $secondCall = New-PrDirectory -OutputFilePath $outputFile
43 $secondCall | Should -Be $firstCall
44 }
45}
46
47Describe 'Resolve-ComparisonReference' {
48 It 'Returns PSCustomObject with Ref and Label properties' {
49 $result = Resolve-ComparisonReference -BaseBranch 'main'
50 $result | Should -BeOfType [PSCustomObject]
51 $result.PSObject.Properties.Name | Should -Contain 'Ref'
52 $result.PSObject.Properties.Name | Should -Contain 'Label'
53 }
54
55 It 'Uses merge-base when remote branch exists' {
56 # This test assumes main branch exists
57 $result = Resolve-ComparisonReference -BaseBranch 'main'
58 $result.Ref | Should -Not -BeNullOrEmpty
59 }
60
61 It 'Should throw when base branch does not exist' {
62 Mock git { $global:LASTEXITCODE = 1; return $null }
63 { Resolve-ComparisonReference -BaseBranch 'nonexistent-branch-xyz' } | Should -Throw '*does not exist*'
64 }
65
66 Context 'UseMergeBase switch' {
67 It 'Resolves merge-base commit when UseMergeBase is set' {
68 $result = Resolve-ComparisonReference -BaseBranch 'HEAD~3' -UseMergeBase
69 $result.Ref | Should -Not -BeNullOrEmpty
70 # merge-base of HEAD and HEAD~3 should be HEAD~3 itself (or its SHA)
71 $result.Ref | Should -Match '^[a-f0-9]+'
72 }
73
74 It 'Falls back to direct ref when merge-base fails' {
75 $script:callCount = 0
76 Mock git {
77 $script:callCount++
78 if ($script:callCount -le 2) {
79 # First calls: rev-parse --verify succeeds
80 $global:LASTEXITCODE = 0
81 return 'abc1234'
82 }
83 # merge-base call fails
84 $global:LASTEXITCODE = 1
85 return $null
86 }
87 $result = Resolve-ComparisonReference -BaseBranch 'some-branch' -UseMergeBase
88 $result.Ref | Should -Not -BeNullOrEmpty
89 }
90
91 It 'Returns direct ref when UseMergeBase is not set' {
92 $result = Resolve-ComparisonReference -BaseBranch 'main'
93 # Without merge-base, ref should be the branch name or origin/branch
94 $result.Ref | Should -Match '(origin/)?main'
95 }
96 }
97}
98
99Describe 'Get-ShortCommitHash' {
100 It 'Returns 7-character hash for HEAD' {
101 $result = Get-ShortCommitHash -Ref 'HEAD'
102 $result | Should -Match '^[a-f0-9]{7,}$'
103 }
104
105 It 'Returns consistent result for same ref' {
106 $first = Get-ShortCommitHash -Ref 'HEAD'
107 $second = Get-ShortCommitHash -Ref 'HEAD'
108 $first | Should -Be $second
109 }
110
111 It 'Should throw when ref resolution fails' {
112 Mock git { $global:LASTEXITCODE = 128; return '' }
113 { Get-ShortCommitHash -Ref 'invalid-ref-xyz' } | Should -Throw "*Failed to resolve ref*"
114 }
115}
116
117Describe 'Get-CommitEntry' {
118 It 'Returns array of formatted commit entries' {
119 $result = Get-CommitEntry -ComparisonRef 'HEAD~1'
120 $result | Should -BeOfType [string]
121 }
122
123 It 'Returns empty array when no commits in range' {
124 $result = Get-CommitEntry -ComparisonRef 'HEAD'
125 $result | Should -BeNullOrEmpty
126 }
127
128 It 'Should throw when commit history retrieval fails' {
129 Mock git { $global:LASTEXITCODE = 128; return $null }
130 { Get-CommitEntry -ComparisonRef 'main' } | Should -Throw '*Failed to retrieve commit history*'
131 }
132}
133
134Describe 'Get-CommitCount' {
135 It 'Returns integer count' {
136 $result = Get-CommitCount -ComparisonRef 'HEAD~5'
137 $result | Should -BeOfType [int]
138 # Merge commits can inflate the count, so just verify it returns a positive integer
139 $result | Should -BeGreaterOrEqual 1
140 }
141
142 It 'Returns 0 when no commits in range' {
143 $result = Get-CommitCount -ComparisonRef 'HEAD'
144 $result | Should -Be 0
145 }
146
147 It 'Should throw when commit count fails' {
148 Mock git { $global:LASTEXITCODE = 128; return '' }
149 { Get-CommitCount -ComparisonRef 'main' } | Should -Throw '*Failed to count commits*'
150 }
151
152 It 'Should return 0 when commit count text is empty' {
153 Mock git { $global:LASTEXITCODE = 0; return '' }
154 $result = Get-CommitCount -ComparisonRef 'main'
155 $result | Should -Be 0
156 }
157}
158
159Describe 'Get-DiffOutput' {
160 It 'Returns array of diff lines' {
161 $result = Get-DiffOutput -ComparisonRef 'HEAD~1'
162 $result | Should -Not -BeNullOrEmpty
163 }
164
165 It 'Excludes markdown when specified' {
166 # Verify the function executes without error when excluding markdown
167 # The result may be empty if only markdown files were changed
168 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeMarkdownDiff } | Should -Not -Throw
169 }
170
171 It 'Should throw when diff output fails' {
172 Mock git { $global:LASTEXITCODE = 128; return $null }
173 { Get-DiffOutput -ComparisonRef 'main' } | Should -Throw '*Failed to retrieve diff output*'
174 }
175
176 Context 'ExcludeExt parameter' {
177 It 'Accepts extension exclusions without error' {
178 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeExt @('yml', 'json') } | Should -Not -Throw
179 }
180
181 It 'Strips leading dots from extensions' {
182 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeExt @('.yml', '.json') } | Should -Not -Throw
183 }
184
185 It 'Accepts empty extension array' {
186 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeExt @() } | Should -Not -Throw
187 }
188 }
189
190 Context 'ExcludePath parameter' {
191 It 'Accepts path exclusions without error' {
192 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludePath @('docs/', '.github/') } | Should -Not -Throw
193 }
194
195 It 'Accepts empty path array' {
196 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludePath @() } | Should -Not -Throw
197 }
198 }
199
200 Context 'Combined exclusion flags' {
201 It 'Accepts markdown, extension, and path exclusions together' {
202 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeMarkdownDiff -ExcludeExt @('yml') -ExcludePath @('docs/') } | Should -Not -Throw
203 }
204 }
205}
206
207Describe 'Get-DiffSummary' {
208 It 'Returns shortstat summary string' {
209 $result = Get-DiffSummary -ComparisonRef 'HEAD~1'
210 $result | Should -BeOfType [string]
211 }
212
213 It 'Should throw when diff summary fails' {
214 Mock git { $global:LASTEXITCODE = 128; return $null }
215 { Get-DiffSummary -ComparisonRef 'main' } | Should -Throw '*Failed to summarize diff output*'
216 }
217
218 It 'Should return "0 files changed" when diff summary is empty' {
219 Mock git { $global:LASTEXITCODE = 0; return '' }
220 $result = Get-DiffSummary -ComparisonRef 'main'
221 $result | Should -Be '0 files changed'
222 }
223
224 Context 'ExcludeExt parameter' {
225 It 'Accepts extension exclusions without error' {
226 { Get-DiffSummary -ComparisonRef 'HEAD~1' -ExcludeExt @('yml', 'json') } | Should -Not -Throw
227 }
228 }
229
230 Context 'ExcludePath parameter' {
231 It 'Accepts path exclusions without error' {
232 { Get-DiffSummary -ComparisonRef 'HEAD~1' -ExcludePath @('docs/') } | Should -Not -Throw
233 }
234 }
235}
236
237Describe 'Get-PrXmlContent' {
238 It 'Returns valid XML string' {
239 $result = Get-PrXmlContent -CurrentBranch 'feature/test' -BaseBranch 'main' -CommitEntries @('commit 1', 'commit 2') -DiffOutput @('diff line 1', 'diff line 2')
240 $result | Should -Not -BeNullOrEmpty
241 $result | Should -Match '<commit_history>'
242 $result | Should -Match '</commit_history>'
243 }
244
245 It 'Includes branch information' {
246 $result = Get-PrXmlContent -CurrentBranch 'feature/my-branch' -BaseBranch 'main' -CommitEntries @() -DiffOutput @()
247 $result | Should -Match 'feature/my-branch'
248 $result | Should -Match 'main'
249 }
250
251 It 'Includes commit entries' {
252 $result = Get-PrXmlContent -CurrentBranch 'feature/test' -BaseBranch 'main' -CommitEntries @('abc123 Test commit') -DiffOutput @()
253 $result | Should -Match 'abc123 Test commit'
254 }
255
256 It 'Handles empty inputs' {
257 $result = Get-PrXmlContent -CurrentBranch 'branch' -BaseBranch 'main' -CommitEntries @() -DiffOutput @()
258 $result | Should -Not -BeNullOrEmpty
259 }
260}
261
262Describe 'Get-LineImpact' {
263 It 'Parses insertions and deletions from shortstat' {
264 $result = Get-LineImpact -DiffSummary '5 files changed, 100 insertions(+), 50 deletions(-)'
265 $result | Should -Be 150
266 }
267
268 It 'Handles insertions only' {
269 $result = Get-LineImpact -DiffSummary '2 files changed, 25 insertions(+)'
270 $result | Should -Be 25
271 }
272
273 It 'Handles deletions only' {
274 $result = Get-LineImpact -DiffSummary '1 file changed, 10 deletions(-)'
275 $result | Should -Be 10
276 }
277
278 It 'Returns 0 for summary without insertions or deletions' {
279 $result = Get-LineImpact -DiffSummary 'no changes'
280 $result | Should -Be 0
281 }
282
283 It 'Returns 0 for no changes' {
284 $result = Get-LineImpact -DiffSummary '0 files changed'
285 $result | Should -Be 0
286 }
287}
288
289Describe 'Get-CurrentBranchOrRef' {
290 BeforeAll {
291 . (Join-Path -Path $PSScriptRoot -ChildPath '../scripts/generate.ps1')
292 }
293
294 It 'Returns branch name when on a branch' {
295 # This test runs in a real git repo, so it should return something
296 $result = Get-CurrentBranchOrRef
297 $result | Should -Not -BeNullOrEmpty
298 $result | Should -BeOfType [string]
299 }
300
301 It 'Returns string starting with detached@ or branch name' {
302 $result = Get-CurrentBranchOrRef
303 # Either a branch name or detached@<sha>
304 ($result -match '^detached@' -or $result -notmatch '^detached@') | Should -BeTrue
305 }
306
307 It 'Should return detached@sha when in detached HEAD state' {
308 # Use call sequence to distinguish git commands (cross-platform safe)
309 $script:gitCallCount = 0
310 Mock git {
311 $script:gitCallCount++
312 if ($script:gitCallCount -eq 1) {
313 # First call: git branch --show-current returns empty (detached)
314 $global:LASTEXITCODE = 0
315 return ''
316 }
317 # Second call: git rev-parse --short HEAD returns SHA
318 $global:LASTEXITCODE = 0
319 return 'abc1234'
320 }
321 $result = Get-CurrentBranchOrRef
322 $result | Should -Be 'detached@abc1234'
323 }
324
325 It 'Should return unknown when both branch and rev-parse fail' {
326 Mock git {
327 $global:LASTEXITCODE = 128
328 return $null
329 }
330 $result = Get-CurrentBranchOrRef
331 $result | Should -Be 'unknown'
332 }
333}
334
335Describe 'Invoke-PrReferenceGeneration' {
336 It 'Uses custom OutputPath when specified' {
337 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
338 $customPath = Join-Path $tempDir 'custom-output/pr-ref.xml'
339
340 try {
341 $result = Invoke-PrReferenceGeneration -BaseBranch 'HEAD~1' -OutputPath $customPath
342 $result | Should -BeOfType [System.IO.FileInfo]
343 $result.FullName | Should -Be (Resolve-Path $customPath).Path
344 Test-Path $customPath | Should -BeTrue
345 }
346 finally {
347 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
348 }
349 }
350
351 It 'Returns FileInfo object' {
352 # Skip if not in a git repo or no commits to compare
353 $commitCount = Get-CommitCount -ComparisonRef 'HEAD~1'
354 if ($commitCount -eq 0) {
355 Set-ItResult -Skipped -Because 'No commits available for comparison'
356 return
357 }
358
359 # Determine available base branch - prefer origin/main, fall back to main, then HEAD~1
360 $baseBranch = $null
361 foreach ($candidate in @('origin/main', 'main', 'HEAD~1')) {
362 & git rev-parse --verify $candidate 2>$null | Out-Null
363 if ($LASTEXITCODE -eq 0) {
364 $baseBranch = $candidate
365 break
366 }
367 }
368
369 if (-not $baseBranch) {
370 Set-ItResult -Skipped -Because 'No suitable base branch available for comparison'
371 return
372 }
373
374 $result = Invoke-PrReferenceGeneration -BaseBranch $baseBranch
375 $result | Should -BeOfType [System.IO.FileInfo]
376 $result.Extension | Should -Be '.xml'
377 }
378
379 It 'Should include markdown exclusion note when ExcludeMarkdownDiff is specified' {
380 # Skip if not in a git repo or no commits
381 $commitCount = Get-CommitCount -ComparisonRef 'HEAD~1'
382 if ($commitCount -eq 0) {
383 Set-ItResult -Skipped -Because 'No commits available for comparison'
384 return
385 }
386
387 $baseBranch = $null
388 foreach ($candidate in @('origin/main', 'main', 'HEAD~1')) {
389 & git rev-parse --verify $candidate 2>$null | Out-Null
390 if ($LASTEXITCODE -eq 0) {
391 $baseBranch = $candidate
392 break
393 }
394 }
395
396 if (-not $baseBranch) {
397 Set-ItResult -Skipped -Because 'No suitable base branch available for comparison'
398 return
399 }
400
401 Mock Write-Host {}
402
403 $result = Invoke-PrReferenceGeneration -BaseBranch $baseBranch -ExcludeMarkdownDiff
404 $result | Should -BeOfType [System.IO.FileInfo]
405
406 # Verify the markdown exclusion note was output
407 Should -Invoke Write-Host -ParameterFilter { $Object -eq 'Note: Markdown files were excluded from diff output' }
408 }
409
410 Context 'MergeBase parameter' {
411 It 'Generates XML when MergeBase is specified' {
412 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
413 $customPath = Join-Path $tempDir 'merge-base-test.xml'
414 try {
415 Mock Write-Host {}
416 $result = Invoke-PrReferenceGeneration -BaseBranch 'HEAD~1' -MergeBase -OutputPath $customPath
417 $result | Should -BeOfType [System.IO.FileInfo]
418 Should -Invoke Write-Host -ParameterFilter { $Object -eq 'Comparison mode: merge-base' }
419 }
420 finally {
421 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
422 }
423 }
424 }
425
426 Context 'ExcludeExt parameter' {
427 It 'Outputs extension exclusion note' {
428 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
429 $customPath = Join-Path $tempDir 'ext-test.xml'
430 try {
431 Mock Write-Host {}
432 $null = Invoke-PrReferenceGeneration -BaseBranch 'HEAD~1' -ExcludeExt @('yml', 'json') -OutputPath $customPath
433 Should -Invoke Write-Host -ParameterFilter { $Object -like '*Extensions excluded*' }
434 }
435 finally {
436 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
437 }
438 }
439 }
440
441 Context 'ExcludePath parameter' {
442 It 'Outputs path exclusion note' {
443 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
444 $customPath = Join-Path $tempDir 'path-test.xml'
445 try {
446 Mock Write-Host {}
447 $null = Invoke-PrReferenceGeneration -BaseBranch 'HEAD~1' -ExcludePath @('docs/') -OutputPath $customPath
448 Should -Invoke Write-Host -ParameterFilter { $Object -like '*Paths excluded*' }
449 }
450 finally {
451 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
452 }
453 }
454 }
455
456 Context 'BaseBranch auto' {
457 It 'Resolves auto to the remote default branch' {
458 Mock Write-Host {}
459 Mock Resolve-DefaultBranch { return 'origin/main' }
460 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
461 $customPath = Join-Path $tempDir 'auto-test.xml'
462 try {
463 $result = Invoke-PrReferenceGeneration -BaseBranch 'auto' -OutputPath $customPath
464 $result | Should -BeOfType [System.IO.FileInfo]
465 Should -Invoke Resolve-DefaultBranch -Times 1
466 }
467 finally {
468 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
469 }
470 }
471 }
472}
473
474Describe 'Large diff warning' {
475 It 'Should output large diff message when line impact exceeds 1000' {
476 Mock Test-GitAvailability {}
477 Mock Get-RepositoryRoot { return (& git rev-parse --show-toplevel).Trim() }
478 Mock Resolve-DefaultBranch { return 'origin/main' }
479 Mock Get-CurrentBranchOrRef { return 'feature/test' }
480 Mock Resolve-ComparisonReference { return [PSCustomObject]@{ Ref = 'HEAD~1'; Label = 'main' } }
481 Mock Get-ShortCommitHash { return 'abc1234' }
482 Mock Get-CommitEntry { return @('<commit hash="abc1234" date="2026-01-01"><message><subject><![CDATA[test]]></subject><body><![CDATA[]]></body></message></commit>') }
483 Mock Get-CommitCount { return 1 }
484 Mock Get-DiffOutput { return @('diff --git a/file.txt b/file.txt') }
485 Mock Get-DiffSummary { return '10 files changed, 800 insertions(+), 500 deletions(-)' }
486 Mock Set-Content {}
487 Mock Get-Content { return @('line1', 'line2') }
488 Mock Get-Item { return [System.IO.FileInfo]::new('/tmp/pr-reference.xml') }
489 Mock Write-Host {}
490
491 $null = Invoke-PrReferenceGeneration -BaseBranch 'main'
492
493 Should -Invoke Write-Host -ParameterFilter {
494 $Object -like '*Large diff detected*'
495 }
496 }
497}
498
499Describe 'Entry-point execution' -Tag 'Integration' {
500 It 'Should exit 0 when executed successfully as a script' {
501 $scriptPath = Join-Path $PSScriptRoot '../scripts/generate.ps1'
502 $null = & pwsh -File $scriptPath -BaseBranch 'HEAD~1' 2>&1
503 $LASTEXITCODE | Should -Be 0
504 }
505
506 It 'Should exit 1 with error message when generation fails' {
507 $scriptPath = Join-Path $PSScriptRoot '../scripts/generate.ps1'
508 $null = & pwsh -File $scriptPath -BaseBranch 'nonexistent-branch-xyz-999' 2>&1
509 $LASTEXITCODE | Should -Be 1
510 }
511}
512