microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/explain-repo-functionality

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

520lines · 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 Mock git {
162 $global:LASTEXITCODE = 0
163 return @('diff --git a/f.txt b/f.txt', '--- a/f.txt', '+++ b/f.txt', '@@ -1 +1 @@', '-old', '+new')
164 }
165 $result = Get-DiffOutput -ComparisonRef 'HEAD~1'
166 $result | Should -Not -BeNullOrEmpty
167 $result.Count | Should -Be 6
168 }
169
170 It 'Executes without error against real repo' {
171 # Real git diff may return empty when merge=ours collapses lock file diffs
172 { Get-DiffOutput -ComparisonRef 'HEAD~1' } | Should -Not -Throw
173 }
174
175 It 'Excludes markdown when specified' {
176 # The result may be empty if only markdown files were changed
177 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeMarkdownDiff } | Should -Not -Throw
178 }
179
180 It 'Should throw when diff output fails' {
181 Mock git { $global:LASTEXITCODE = 128; return $null }
182 { Get-DiffOutput -ComparisonRef 'main' } | Should -Throw '*Failed to retrieve diff output*'
183 }
184
185 Context 'ExcludeExt parameter' {
186 It 'Accepts extension exclusions without error' {
187 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeExt @('yml', 'json') } | Should -Not -Throw
188 }
189
190 It 'Strips leading dots from extensions' {
191 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeExt @('.yml', '.json') } | Should -Not -Throw
192 }
193
194 It 'Accepts empty extension array' {
195 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeExt @() } | Should -Not -Throw
196 }
197 }
198
199 Context 'ExcludePath parameter' {
200 It 'Accepts path exclusions without error' {
201 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludePath @('docs/', '.github/') } | Should -Not -Throw
202 }
203
204 It 'Accepts empty path array' {
205 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludePath @() } | Should -Not -Throw
206 }
207 }
208
209 Context 'Combined exclusion flags' {
210 It 'Accepts markdown, extension, and path exclusions together' {
211 { Get-DiffOutput -ComparisonRef 'HEAD~1' -ExcludeMarkdownDiff -ExcludeExt @('yml') -ExcludePath @('docs/') } | Should -Not -Throw
212 }
213 }
214}
215
216Describe 'Get-DiffSummary' {
217 It 'Returns shortstat summary string' {
218 $result = Get-DiffSummary -ComparisonRef 'HEAD~1'
219 $result | Should -BeOfType [string]
220 }
221
222 It 'Should throw when diff summary fails' {
223 Mock git { $global:LASTEXITCODE = 128; return $null }
224 { Get-DiffSummary -ComparisonRef 'main' } | Should -Throw '*Failed to summarize diff output*'
225 }
226
227 It 'Should return "0 files changed" when diff summary is empty' {
228 Mock git { $global:LASTEXITCODE = 0; return '' }
229 $result = Get-DiffSummary -ComparisonRef 'main'
230 $result | Should -Be '0 files changed'
231 }
232
233 Context 'ExcludeExt parameter' {
234 It 'Accepts extension exclusions without error' {
235 { Get-DiffSummary -ComparisonRef 'HEAD~1' -ExcludeExt @('yml', 'json') } | Should -Not -Throw
236 }
237 }
238
239 Context 'ExcludePath parameter' {
240 It 'Accepts path exclusions without error' {
241 { Get-DiffSummary -ComparisonRef 'HEAD~1' -ExcludePath @('docs/') } | Should -Not -Throw
242 }
243 }
244}
245
246Describe 'Get-PrXmlContent' {
247 It 'Returns valid XML string' {
248 $result = Get-PrXmlContent -CurrentBranch 'feature/test' -BaseBranch 'main' -CommitEntries @('commit 1', 'commit 2') -DiffOutput @('diff line 1', 'diff line 2')
249 $result | Should -Not -BeNullOrEmpty
250 $result | Should -Match '<commit_history>'
251 $result | Should -Match '</commit_history>'
252 }
253
254 It 'Includes branch information' {
255 $result = Get-PrXmlContent -CurrentBranch 'feature/my-branch' -BaseBranch 'main' -CommitEntries @() -DiffOutput @()
256 $result | Should -Match 'feature/my-branch'
257 $result | Should -Match 'main'
258 }
259
260 It 'Includes commit entries' {
261 $result = Get-PrXmlContent -CurrentBranch 'feature/test' -BaseBranch 'main' -CommitEntries @('abc123 Test commit') -DiffOutput @()
262 $result | Should -Match 'abc123 Test commit'
263 }
264
265 It 'Handles empty inputs' {
266 $result = Get-PrXmlContent -CurrentBranch 'branch' -BaseBranch 'main' -CommitEntries @() -DiffOutput @()
267 $result | Should -Not -BeNullOrEmpty
268 }
269}
270
271Describe 'Get-LineImpact' {
272 It 'Parses insertions and deletions from shortstat' {
273 $result = Get-LineImpact -DiffSummary '5 files changed, 100 insertions(+), 50 deletions(-)'
274 $result | Should -Be 150
275 }
276
277 It 'Handles insertions only' {
278 $result = Get-LineImpact -DiffSummary '2 files changed, 25 insertions(+)'
279 $result | Should -Be 25
280 }
281
282 It 'Handles deletions only' {
283 $result = Get-LineImpact -DiffSummary '1 file changed, 10 deletions(-)'
284 $result | Should -Be 10
285 }
286
287 It 'Returns 0 for summary without insertions or deletions' {
288 $result = Get-LineImpact -DiffSummary 'no changes'
289 $result | Should -Be 0
290 }
291
292 It 'Returns 0 for no changes' {
293 $result = Get-LineImpact -DiffSummary '0 files changed'
294 $result | Should -Be 0
295 }
296}
297
298Describe 'Get-CurrentBranchOrRef' {
299 BeforeAll {
300 . (Join-Path -Path $PSScriptRoot -ChildPath '../scripts/generate.ps1')
301 }
302
303 It 'Returns branch name when on a branch' {
304 # This test runs in a real git repo, so it should return something
305 $result = Get-CurrentBranchOrRef
306 $result | Should -Not -BeNullOrEmpty
307 $result | Should -BeOfType [string]
308 }
309
310 It 'Returns string starting with detached@ or branch name' {
311 $result = Get-CurrentBranchOrRef
312 # Either a branch name or detached@<sha>
313 ($result -match '^detached@' -or $result -notmatch '^detached@') | Should -BeTrue
314 }
315
316 It 'Should return detached@sha when in detached HEAD state' {
317 # Use call sequence to distinguish git commands (cross-platform safe)
318 $script:gitCallCount = 0
319 Mock git {
320 $script:gitCallCount++
321 if ($script:gitCallCount -eq 1) {
322 # First call: git branch --show-current returns empty (detached)
323 $global:LASTEXITCODE = 0
324 return ''
325 }
326 # Second call: git rev-parse --short HEAD returns SHA
327 $global:LASTEXITCODE = 0
328 return 'abc1234'
329 }
330 $result = Get-CurrentBranchOrRef
331 $result | Should -Be 'detached@abc1234'
332 }
333
334 It 'Should return unknown when both branch and rev-parse fail' {
335 Mock git {
336 $global:LASTEXITCODE = 128
337 return $null
338 }
339 $result = Get-CurrentBranchOrRef
340 $result | Should -Be 'unknown'
341 }
342}
343
344Describe 'Invoke-PrReferenceGeneration' {
345 It 'Uses custom OutputPath when specified' {
346 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
347 $customPath = Join-Path $tempDir 'custom-output/pr-ref.xml'
348
349 try {
350 $result = Invoke-PrReferenceGeneration -BaseBranch 'HEAD~1' -OutputPath $customPath
351 $result | Should -BeOfType [System.IO.FileInfo]
352 $result.FullName | Should -Be (Resolve-Path $customPath).Path
353 Test-Path $customPath | Should -BeTrue
354 }
355 finally {
356 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
357 }
358 }
359
360 It 'Returns FileInfo object' {
361 # Skip if not in a git repo or no commits to compare
362 $commitCount = Get-CommitCount -ComparisonRef 'HEAD~1'
363 if ($commitCount -eq 0) {
364 Set-ItResult -Skipped -Because 'No commits available for comparison'
365 return
366 }
367
368 # Determine available base branch - prefer origin/main, fall back to main, then HEAD~1
369 $baseBranch = $null
370 foreach ($candidate in @('origin/main', 'main', 'HEAD~1')) {
371 & git rev-parse --verify $candidate 2>$null | Out-Null
372 if ($LASTEXITCODE -eq 0) {
373 $baseBranch = $candidate
374 break
375 }
376 }
377
378 if (-not $baseBranch) {
379 Set-ItResult -Skipped -Because 'No suitable base branch available for comparison'
380 return
381 }
382
383 $result = Invoke-PrReferenceGeneration -BaseBranch $baseBranch
384 $result | Should -BeOfType [System.IO.FileInfo]
385 $result.Extension | Should -Be '.xml'
386 }
387
388 It 'Should include markdown exclusion note when ExcludeMarkdownDiff is specified' {
389 # Skip if not in a git repo or no commits
390 $commitCount = Get-CommitCount -ComparisonRef 'HEAD~1'
391 if ($commitCount -eq 0) {
392 Set-ItResult -Skipped -Because 'No commits available for comparison'
393 return
394 }
395
396 $baseBranch = $null
397 foreach ($candidate in @('origin/main', 'main', 'HEAD~1')) {
398 & git rev-parse --verify $candidate 2>$null | Out-Null
399 if ($LASTEXITCODE -eq 0) {
400 $baseBranch = $candidate
401 break
402 }
403 }
404
405 if (-not $baseBranch) {
406 Set-ItResult -Skipped -Because 'No suitable base branch available for comparison'
407 return
408 }
409
410 Mock Write-Host {}
411
412 $result = Invoke-PrReferenceGeneration -BaseBranch $baseBranch -ExcludeMarkdownDiff
413 $result | Should -BeOfType [System.IO.FileInfo]
414
415 # Verify the markdown exclusion note was output
416 Should -Invoke Write-Host -ParameterFilter { $Object -eq 'Note: Markdown files were excluded from diff output' }
417 }
418
419 Context 'MergeBase parameter' {
420 It 'Generates XML when MergeBase is specified' {
421 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
422 $customPath = Join-Path $tempDir 'merge-base-test.xml'
423 try {
424 Mock Write-Host {}
425 $result = Invoke-PrReferenceGeneration -BaseBranch 'HEAD~1' -MergeBase -OutputPath $customPath
426 $result | Should -BeOfType [System.IO.FileInfo]
427 Should -Invoke Write-Host -ParameterFilter { $Object -eq 'Comparison mode: merge-base' }
428 }
429 finally {
430 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
431 }
432 }
433 }
434
435 Context 'ExcludeExt parameter' {
436 It 'Outputs extension exclusion note' {
437 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
438 $customPath = Join-Path $tempDir 'ext-test.xml'
439 try {
440 Mock Write-Host {}
441 $null = Invoke-PrReferenceGeneration -BaseBranch 'HEAD~1' -ExcludeExt @('yml', 'json') -OutputPath $customPath
442 Should -Invoke Write-Host -ParameterFilter { $Object -like '*Extensions excluded*' }
443 }
444 finally {
445 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
446 }
447 }
448 }
449
450 Context 'ExcludePath parameter' {
451 It 'Outputs path exclusion note' {
452 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
453 $customPath = Join-Path $tempDir 'path-test.xml'
454 try {
455 Mock Write-Host {}
456 $null = Invoke-PrReferenceGeneration -BaseBranch 'HEAD~1' -ExcludePath @('docs/') -OutputPath $customPath
457 Should -Invoke Write-Host -ParameterFilter { $Object -like '*Paths excluded*' }
458 }
459 finally {
460 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
461 }
462 }
463 }
464
465 Context 'BaseBranch auto' {
466 It 'Resolves auto to the remote default branch' {
467 Mock Write-Host {}
468 Mock Resolve-DefaultBranch { return 'origin/main' }
469 $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
470 $customPath = Join-Path $tempDir 'auto-test.xml'
471 try {
472 $result = Invoke-PrReferenceGeneration -BaseBranch 'auto' -OutputPath $customPath
473 $result | Should -BeOfType [System.IO.FileInfo]
474 Should -Invoke Resolve-DefaultBranch -Times 1
475 }
476 finally {
477 Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
478 }
479 }
480 }
481}
482
483Describe 'Large diff warning' {
484 It 'Should output large diff message when line impact exceeds 1000' {
485 Mock Test-GitAvailability {}
486 Mock Get-RepositoryRoot { return (& git rev-parse --show-toplevel).Trim() }
487 Mock Resolve-DefaultBranch { return 'origin/main' }
488 Mock Get-CurrentBranchOrRef { return 'feature/test' }
489 Mock Resolve-ComparisonReference { return [PSCustomObject]@{ Ref = 'HEAD~1'; Label = 'main' } }
490 Mock Get-ShortCommitHash { return 'abc1234' }
491 Mock Get-CommitEntry { return @('<commit hash="abc1234" date="2026-01-01"><message><subject><![CDATA[test]]></subject><body><![CDATA[]]></body></message></commit>') }
492 Mock Get-CommitCount { return 1 }
493 Mock Get-DiffOutput { return @('diff --git a/file.txt b/file.txt') }
494 Mock Get-DiffSummary { return '10 files changed, 800 insertions(+), 500 deletions(-)' }
495 Mock Set-Content {}
496 Mock Get-Content { return @('line1', 'line2') }
497 Mock Get-Item { return [System.IO.FileInfo]::new('/tmp/pr-reference.xml') }
498 Mock Write-Host {}
499
500 $null = Invoke-PrReferenceGeneration -BaseBranch 'main'
501
502 Should -Invoke Write-Host -ParameterFilter {
503 $Object -like '*Large diff detected*'
504 }
505 }
506}
507
508Describe 'Entry-point execution' -Tag 'Integration' {
509 It 'Should exit 0 when executed successfully as a script' {
510 $scriptPath = Join-Path $PSScriptRoot '../scripts/generate.ps1'
511 $null = & pwsh -File $scriptPath -BaseBranch 'HEAD~1' 2>&1
512 $LASTEXITCODE | Should -Be 0
513 }
514
515 It 'Should exit 1 with error message when generation fails' {
516 $scriptPath = Join-Path $PSScriptRoot '../scripts/generate.ps1'
517 $null = & pwsh -File $scriptPath -BaseBranch 'nonexistent-branch-xyz-999' 2>&1
518 $LASTEXITCODE | Should -Be 1
519 }
520}