Tests/New-GitWorktree.Tests.ps1
|
BeforeAll { Import-Module "$PSScriptRoot\..\GittHelpers.psd1" -Force } Describe 'New-GitWorktree' { BeforeEach { $script:BranchCreated = $null $script:WtPathUsed = $null Mock Get-GitRoot { return $TestDrive } -ModuleName GittHelpers Mock Get-WorktreeList { return @() } -ModuleName GittHelpers Mock Get-GitAuthorSlug { return 'snkemp' } -ModuleName GittHelpers Mock Open-WorktreeSolution { } -ModuleName GittHelpers Mock Open-WorktreeTerminal { } -ModuleName GittHelpers Mock git { $global:LASTEXITCODE = 0 if ($args -contains 'symbolic-ref') { return 'main' } if ($args -contains 'worktree' -and $args -contains 'add' -and $args -contains '-b') { $bIdx = [array]::IndexOf([object[]]$args, '-b') if ($bIdx -ge 0) { $script:BranchCreated = $args[$bIdx + 1] $script:WtPathUsed = $args[$bIdx + 2] } } } -ModuleName GittHelpers Mock New-Item { } -ModuleName GittHelpers } It 'Uses feature/ prefix for -Feature parameter set' { { New-GitWorktree -Feature 'login' -WhatIf } | Should -Not -Throw } It 'Includes author segment in feature branch name' { New-GitWorktree -Feature 'login' -Confirm:$false $script:BranchCreated | Should -Be 'feature/snkemp/login' } It 'Feature worktree path does NOT include author' { New-GitWorktree -Feature 'login' -Confirm:$false $script:WtPathUsed | Should -Not -Match 'snkemp' $script:WtPathUsed | Should -Match 'feature' } It 'Includes author segment in bug branch name' { New-GitWorktree -Bug 'null-ref' -Confirm:$false $script:BranchCreated | Should -Be 'bug/snkemp/null-ref' } It 'Bug worktree path does NOT include author' { New-GitWorktree -Bug 'null-ref' -Confirm:$false $script:WtPathUsed | Should -Not -Match 'snkemp' } It 'Includes author segment in temp branch name' { New-GitWorktree -Temp 'experiment' -Confirm:$false $script:BranchCreated | Should -Be 'temp/snkemp/experiment' } It 'Does NOT include author segment for hotfix (prod path)' { New-GitWorktree -Hotfix '2.1.1' -Confirm:$false $script:BranchCreated | Should -Be 'hotfix/2.1.1' } It 'Does NOT include author segment for release (prod path)' { New-GitWorktree -Release '3.0.0' -Confirm:$false $script:BranchCreated | Should -Be 'release/3.0.0' } It 'Suppresses author with -NoAuthor on dev paths' { New-GitWorktree -Feature 'login' -NoAuthor -Confirm:$false $script:BranchCreated | Should -Be 'feature/login' } It '-Author overrides the author slug in the branch name' { New-GitWorktree -Feature 'login' -Author 'alice' -Confirm:$false $script:BranchCreated | Should -Be 'feature/alice/login' } It '-Author slugifies the supplied value' { New-GitWorktree -Feature 'login' -Author 'Alice Smith' -Confirm:$false $script:BranchCreated | Should -Be 'feature/alice-smith/login' } It 'Builds correct branch name from -Hotfix and version (dots preserved)' { $slug = & (Get-Module GittHelpers) { ConvertTo-SafeBranchName '2.1.1' } $slug | Should -Be '2.1.1' } It 'Appends description slug to branch name' { $slug = & (Get-Module GittHelpers) { $n = ConvertTo-SafeBranchName 'login' $d = ConvertTo-SafeBranchName 'Add OAuth login' "$n-$d" } $slug | Should -Be 'login-add-oauth-login' } It 'Throws when no source branch can be found' { Mock git { $global:LASTEXITCODE = 1; return $null } -ModuleName GittHelpers { New-GitWorktree -Feature 'x' -SourceBranch 'nonexistent' } | Should -Throw } It 'Returns a result object with Path, Branch, and SourceBranch' { $result = New-GitWorktree -Feature 'login' -Confirm:$false $result.Branch | Should -Be 'feature/snkemp/login' $result.SourceBranch | Should -Be 'main' $result.Path | Should -Not -BeNullOrEmpty } } Describe 'New-GitWorktree -Develop parameter set' { BeforeEach { $script:BranchCreated = $null $script:WtPathUsed = $null Mock Get-GitRoot { return $TestDrive } -ModuleName GittHelpers Mock Get-GitAuthorSlug { return 'snkemp' } -ModuleName GittHelpers Mock Open-WorktreeSolution { } -ModuleName GittHelpers Mock Open-WorktreeTerminal { } -ModuleName GittHelpers Mock git { $global:LASTEXITCODE = 0 if ($args -contains 'symbolic-ref') { return 'master' } if ($args -contains 'worktree' -and $args -contains 'add' -and $args -contains '-b') { $bIdx = [array]::IndexOf([object[]]$args, '-b') if ($bIdx -ge 0) { $script:BranchCreated = $args[$bIdx + 1] $script:WtPathUsed = $args[$bIdx + 2] } } } -ModuleName GittHelpers Mock New-Item { } -ModuleName GittHelpers } It 'Develop branch always includes author' { New-GitWorktree -Develop 'payments' -Confirm:$false $script:BranchCreated | Should -Be 'develop/snkemp/payments' } It 'Develop worktree path does NOT include author' { New-GitWorktree -Develop 'payments' -Confirm:$false $script:WtPathUsed | Should -Not -Match 'snkemp' $script:WtPathUsed | Should -Match 'develop' } It '-NoAuthor has no effect on Develop (author always present)' { New-GitWorktree -Develop 'payments' -NoAuthor -Confirm:$false $script:BranchCreated | Should -Be 'develop/snkemp/payments' } } Describe 'New-GitWorktree parameter sets' { It 'All branch-type parameters are in distinct parameter sets' { $cmd = Get-Command New-GitWorktree $sets = $cmd.ParameterSets | Select-Object -ExpandProperty Name $sets | Should -Contain 'Feature' $sets | Should -Contain 'Develop' $sets | Should -Contain 'Hotfix' $sets | Should -Contain 'Release' $sets | Should -Contain 'Bug' $sets | Should -Contain 'Temp' } } Describe 'New-GitWorktree -NoSolution behavior' { BeforeEach { Mock Get-GitRoot { return $TestDrive } -ModuleName GittHelpers Mock Get-GitAuthorSlug { return 'snkemp' } -ModuleName GittHelpers Mock git { $global:LASTEXITCODE = 0 if ($args -contains 'symbolic-ref') { return 'main' } } -ModuleName GittHelpers Mock New-Item { } -ModuleName GittHelpers Mock Open-WorktreeSolution { } -ModuleName GittHelpers Mock Open-WorktreeTerminal { } -ModuleName GittHelpers } It 'Calls Open-WorktreeSolution when -NoSolution is not set' { New-GitWorktree -Feature 'login' -Confirm:$false Should -Invoke Open-WorktreeSolution -ModuleName GittHelpers -Times 1 } It 'Does not call Open-WorktreeSolution when -NoSolution is set' { New-GitWorktree -Feature 'login' -NoSolution -Confirm:$false Should -Invoke Open-WorktreeSolution -ModuleName GittHelpers -Times 0 } } Describe 'New-GitWorktree -OpenTerminal behavior' { BeforeEach { Mock Get-GitRoot { return $TestDrive } -ModuleName GittHelpers Mock Get-GitAuthorSlug { return 'snkemp' } -ModuleName GittHelpers Mock git { $global:LASTEXITCODE = 0 if ($args -contains 'symbolic-ref') { return 'main' } } -ModuleName GittHelpers Mock New-Item { } -ModuleName GittHelpers Mock Open-WorktreeSolution { } -ModuleName GittHelpers Mock Open-WorktreeTerminal { } -ModuleName GittHelpers } It 'Does not call Open-WorktreeTerminal when -OpenTerminal is not set' { New-GitWorktree -Feature 'login' -Confirm:$false Should -Invoke Open-WorktreeTerminal -ModuleName GittHelpers -Times 0 } It 'Calls Open-WorktreeTerminal when -OpenTerminal is set' { New-GitWorktree -Feature 'login' -OpenTerminal -Confirm:$false Should -Invoke Open-WorktreeTerminal -ModuleName GittHelpers -Times 1 } } Describe 'Open-WorktreeSolution' { BeforeEach { Mock Start-Process { } -ModuleName GittHelpers Mock Read-Host { return '0' } -ModuleName GittHelpers } It 'Does nothing when no solution files are found' { Mock Get-ChildItem { return @() } -ModuleName GittHelpers ` -ParameterFilter { $Include -contains '*.sln' } & (Get-Module GittHelpers) { Open-WorktreeSolution -WorktreePath 'C:\fake' } Should -Invoke Start-Process -ModuleName GittHelpers -Times 0 } It 'Opens single solution without prompting' { $sln = [PSCustomObject]@{ FullName = 'C:\fake\App.sln'; Name = 'App.sln' } Mock Get-ChildItem { return @($sln) } -ModuleName GittHelpers ` -ParameterFilter { $Include -contains '*.sln' } & (Get-Module GittHelpers) { Open-WorktreeSolution -WorktreePath 'C:\fake' } Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 ` -ParameterFilter { $FilePath -eq 'C:\fake\App.sln' } Should -Invoke Read-Host -ModuleName GittHelpers -Times 0 } It 'Prompts user and opens selected file when multiple solutions found' { $slns = @( [PSCustomObject]@{ FullName = 'C:\fake\App.sln'; Name = 'App.sln' }, [PSCustomObject]@{ FullName = 'C:\fake\Tests.sln'; Name = 'Tests.sln' } ) Mock Get-ChildItem { return $slns } -ModuleName GittHelpers ` -ParameterFilter { $Include -contains '*.sln' } Mock Read-Host { return '1' } -ModuleName GittHelpers & (Get-Module GittHelpers) { Open-WorktreeSolution -WorktreePath 'C:\fake' } Should -Invoke Read-Host -ModuleName GittHelpers -Times 1 Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 ` -ParameterFilter { $FilePath -eq 'C:\fake\Tests.sln' } } It 'Re-prompts on invalid input until a valid selection is made' { $slns = @( [PSCustomObject]@{ FullName = 'C:\fake\App.sln'; Name = 'App.sln' }, [PSCustomObject]@{ FullName = 'C:\fake\Tests.sln'; Name = 'Tests.sln' } ) Mock Get-ChildItem { return $slns } -ModuleName GittHelpers ` -ParameterFilter { $Include -contains '*.sln' } $script:ReadCount = 0 Mock Read-Host { $script:ReadCount++ if ($script:ReadCount -eq 1) { return 'nope' } else { return '0' } } -ModuleName GittHelpers & (Get-Module GittHelpers) { Open-WorktreeSolution -WorktreePath 'C:\fake' } Should -Invoke Read-Host -ModuleName GittHelpers -Times 2 Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 ` -ParameterFilter { $FilePath -eq 'C:\fake\App.sln' } } } Describe 'Open-WorktreeTerminal' { BeforeEach { Mock Start-Process { } -ModuleName GittHelpers } It 'Uses Windows Terminal when wt is available' { Mock Get-Command { return [PSCustomObject]@{ Name = 'wt' } } -ModuleName GittHelpers ` -ParameterFilter { $Name -eq 'wt' } & (Get-Module GittHelpers) { Open-WorktreeTerminal -WorktreePath 'C:\fake' } Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 ` -ParameterFilter { $FilePath -eq 'wt' } } It 'Falls back to pwsh when wt is not available but pwsh is' { Mock Get-Command { if ($Name -eq 'wt') { return $null } if ($Name -eq 'pwsh') { return [PSCustomObject]@{ Name = 'pwsh' } } } -ModuleName GittHelpers & (Get-Module GittHelpers) { Open-WorktreeTerminal -WorktreePath 'C:\fake' } Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 ` -ParameterFilter { $FilePath -eq 'pwsh' } } It 'Falls back to powershell when neither wt nor pwsh is available' { Mock Get-Command { return $null } -ModuleName GittHelpers & (Get-Module GittHelpers) { Open-WorktreeTerminal -WorktreePath 'C:\fake' } Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 ` -ParameterFilter { $FilePath -eq 'powershell' } } } Describe 'Get-GitAuthorSlug' { AfterEach { $env:GIT_USER_SHORTNAME = $null } It 'Prefers $env:GIT_USER_SHORTNAME over everything else' { $env:GIT_USER_SHORTNAME = 'myshortname' Mock git { $global:LASTEXITCODE = 0; return 'Other Name' } -ModuleName GittHelpers $result = & (Get-Module GittHelpers) { Get-GitAuthorSlug } $result | Should -Be 'myshortname' } It 'Slugifies $env:GIT_USER_SHORTNAME' { $env:GIT_USER_SHORTNAME = 'Spencer Kemp' Mock git { $global:LASTEXITCODE = 0 } -ModuleName GittHelpers $result = & (Get-Module GittHelpers) { Get-GitAuthorSlug } $result | Should -Be 'spencer-kemp' } It 'Falls back to git user.name when GIT_USER_SHORTNAME is not set' { $env:GIT_USER_SHORTNAME = $null Mock git { $global:LASTEXITCODE = 0 if ($args -contains 'user.name') { return 'snkemp' } } -ModuleName GittHelpers $result = & (Get-Module GittHelpers) { Get-GitAuthorSlug } $result | Should -Be 'snkemp' } It 'Falls back to $env:USERNAME when both other sources are empty' { $env:GIT_USER_SHORTNAME = $null Mock git { $global:LASTEXITCODE = 0; return $null } -ModuleName GittHelpers $result = & (Get-Module GittHelpers) { Get-GitAuthorSlug } $expected = $env:USERNAME.ToLowerInvariant() -replace '[^\w\-\.]+', '-' $result | Should -Be $expected } } |