Skip to content

Commit fbbece4

Browse files
committed
fix(test): keep unit/CI runs non-interactive
Pin Pester to 5.7.x, force -NonInteractive child pwsh, and refuse auto-install paths that can hang waiting for input under tests.
1 parent b3ee4da commit fbbece4

14 files changed

Lines changed: 257 additions & 92 deletions

.github/workflows/test-pester.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,20 @@ jobs:
8888
Register-PSRepository -Default
8989
}
9090
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
91-
Install-Module -Name Pester -MinimumVersion 5.0.0 -MaximumVersion 5.99.99 -Force -Scope CurrentUser -AllowClobber
92-
# Prefer 5.7.x when multiple 5.x builds are present; reject Pester 6+ (breaks coverage tracer teardown / mocks).
91+
# Pin to 5.7.x: Pester 5.9+ regresses Mock scoping ("Mock data are not setup for this scope")
92+
# and TestDrive collisions across sequential containers; Pester 6+ breaks tracer teardown.
93+
Install-Module -Name Pester -MinimumVersion 5.7.0 -MaximumVersion 5.7.99 -Force -Scope CurrentUser -AllowClobber
9394
$pester5 = Get-Module -ListAvailable Pester |
94-
Where-Object { $_.Version -ge [version]'5.0.0' -and $_.Version -lt [version]'6.0.0' } |
95+
Where-Object { $_.Version -ge [version]'5.7.0' -and $_.Version -lt [version]'5.8.0' } |
9596
Sort-Object Version -Descending |
9697
Select-Object -First 1
97-
if (-not $pester5) { throw 'Pester 5.x is required but was not installed' }
98+
if (-not $pester5) { throw 'Pester 5.7.x is required but was not installed' }
9899
Import-Module Pester -RequiredVersion $pester5.Version -Force
99100
Write-Host "Installed Pester $($pester5.Version)"
100101
- name: Run Pester shard
101102
shell: pwsh
102103
run: |
103-
pwsh -NoProfile -File scripts/utils/code-quality/run-pester-ci-shard.ps1 -Shard '${{ matrix.shard }}' -Quiet
104+
pwsh -NoProfile -NonInteractive -File scripts/utils/code-quality/run-pester-ci-shard.ps1 -Shard '${{ matrix.shard }}' -Quiet
104105
- name: Upload test results
105106
if: always()
106107
uses: actions/upload-artifact@v7

scripts/utils/code-quality/run-pester-ci-shard.ps1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ param(
4444

4545
Set-StrictMode -Version Latest
4646
$ErrorActionPreference = 'Stop'
47+
$ConfirmPreference = 'None'
48+
$global:ConfirmPreference = 'None'
49+
$env:PS_PROFILE_NONINTERACTIVE = '1'
50+
$env:PS_PROFILE_TEST_MODE = '1'
51+
$env:PS_PROFILE_SUPPRESS_CONFIRMATIONS = '1'
4752

4853
$runner = Join-Path $RepoRoot 'scripts' 'utils' 'code-quality' 'run-pester.ps1'
4954
$toolsBatch = Join-Path $RepoRoot 'scripts' 'utils' 'code-quality' 'run-tools-integration-batch.ps1'

scripts/utils/code-quality/run-pester.ps1

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -886,13 +886,13 @@ $global:ConfirmPreference = 'None'
886886
# Initialize output utilities
887887
Initialize-OutputUtils -RepoRoot $repoRoot
888888

889-
# Ensure Pester 5.x is available and imported (Pester 6+ breaks CodeCoverage tracer teardown
890-
# when profile fragments install PostCommandLookupAction, and changes Mock ParameterFilter semantics).
891-
# Do NOT call Ensure-ModuleAvailable for Pester — Install-RequiredModule has no MaximumVersion and
892-
# may install Pester 6+. Install/import only the 5.x range explicitly.
889+
# Ensure Pester 5.7.x is available and imported.
890+
# - 5.9+ regresses Mock scoping ("Mock data are not setup for this scope") and TestDrive collisions
891+
# - 6+ breaks CodeCoverage tracer teardown when profile fragments install PostCommandLookupAction
892+
# Do NOT call Ensure-ModuleAvailable for Pester — Install-RequiredModule has no MaximumVersion.
893893
Write-Host "Checking Pester availability..." -ForegroundColor Yellow
894-
$requiredPesterVersion = [version]'5.0.0'
895-
$maxPesterVersion = [version]'5.99.99'
894+
$requiredPesterVersion = [version]'5.7.0'
895+
$maxPesterVersion = [version]'5.7.99'
896896

897897
$installedPester = Get-Module -ListAvailable -Name 'Pester' |
898898
Where-Object { $_.Version -ge $requiredPesterVersion -and $_.Version -le $maxPesterVersion } |
@@ -1231,7 +1231,7 @@ try {
12311231
# Handle Interactive mode
12321232
if ($Interactive) {
12331233
$interactiveInputAvailable = $false
1234-
if ($env:PS_PROFILE_NONINTERACTIVE -ne '1' -and $env:CI -ne 'true' -and $env:GITHUB_ACTIONS -ne 'true') {
1234+
if ($env:PS_PROFILE_NONINTERACTIVE -ne '1' -and $env:PS_PROFILE_TEST_MODE -ne '1' -and $env:CI -ne 'true' -and $env:GITHUB_ACTIONS -ne 'true') {
12351235
try {
12361236
$interactiveInputAvailable = -not [Console]::IsInputRedirected
12371237
}

scripts/utils/docs/generate-changelog.ps1

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ Write-ScriptMessage -Message "Generating changelog..."
9292
$hasGitCliff = Test-CommandAvailable -CommandName 'git-cliff'
9393

9494
if (-not $hasGitCliff) {
95+
# Never auto-install in CI/unit tests — cargo/winget can prompt or run for minutes.
96+
$nonInteractive = (
97+
$env:PS_PROFILE_NONINTERACTIVE -eq '1' -or
98+
$env:PS_PROFILE_TEST_MODE -eq '1' -or
99+
$env:CI -eq 'true' -or
100+
$env:GITHUB_ACTIONS -eq 'true'
101+
)
102+
if ($nonInteractive) {
103+
Exit-WithCode -ExitCode $EXIT_SETUP_ERROR -Message 'git-cliff is required but not installed (auto-install disabled in non-interactive mode)'
104+
}
105+
95106
Write-ScriptMessage -Message "git-cliff not found. Installing..."
96107

97108
# Try to install git-cliff

tests/TestSupport/TestExecution.ps1

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ function Invoke-TestScriptFile {
4848
}
4949

5050
try {
51-
$output = & pwsh -NoProfile -File $ScriptPath @ArgumentList 2>&1 | Out-String
51+
# Always non-interactive: unit/CI child scripts must never wait on Read-Host / ShouldProcess prompts.
52+
$output = & pwsh -NoProfile -NonInteractive -File $ScriptPath @ArgumentList 2>&1 | Out-String
5253
return [pscustomobject]@{
5354
ExitCode = $LASTEXITCODE
5455
Output = $output
@@ -80,6 +81,9 @@ function Invoke-TestPwshScript {
8081
$escapedRepoRoot = $repoRoot.Replace("'", "''")
8182
$scriptPrefix = @"
8283
`$env:PS_PROFILE_TEST_MODE = '1'
84+
`$env:PS_PROFILE_NONINTERACTIVE = '1'
85+
`$env:PS_PROFILE_SUPPRESS_CONFIRMATIONS = '1'
86+
`$ConfirmPreference = 'None'
8387
`$env:PS_PROFILE_REPO_ROOT = '$escapedRepoRoot'
8488
"@
8589

@@ -110,7 +114,7 @@ function Invoke-TestPwshScript {
110114
throw "pwsh command not found. PowerShell Core must be installed to use this function."
111115
}
112116

113-
$output = & pwsh -NoProfile -File $tempFile 2>&1
117+
$output = & pwsh -NoProfile -NonInteractive -File $tempFile 2>&1
114118
$exitCode = $LASTEXITCODE
115119

116120
if ($exitCode -ne 0) {

tests/unit/library/chocolatey/library-chocolatey-detection-extended.tests.ps1

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,46 @@ function script:Clear-ChocolateyTestEnvironment {
4242
}
4343
}
4444

45+
function script:Disable-ChocolateyDefaultLocations {
46+
<#
47+
.SYNOPSIS
48+
Isolate detection from a host Chocolatey install (ProgramData + hardcoded fallback).
49+
#>
50+
$emptyProgramData = Join-Path $script:TempDir 'empty-programdata'
51+
if (-not (Test-Path -LiteralPath $emptyProgramData)) {
52+
New-Item -ItemType Directory -Path $emptyProgramData -Force | Out-Null
53+
}
54+
Mock-EnvironmentVariable -Name 'ProgramData' -Value $emptyProgramData
55+
56+
Mock Get-Command {
57+
$cmdName = $Name
58+
if ([string]::IsNullOrWhiteSpace($cmdName) -and $args.Count -gt 0) {
59+
$cmdName = [string]$args[0]
60+
}
61+
if ($cmdName -eq 'Test-ValidPath') {
62+
return $null
63+
}
64+
return Microsoft.PowerShell.Core\Get-Command @PSBoundParameters
65+
} -ModuleName ChocolateyDetection
66+
67+
Mock Test-Path {
68+
$target = $null
69+
if ($PSBoundParameters.ContainsKey('LiteralPath') -and $LiteralPath) {
70+
$target = [string]$LiteralPath
71+
}
72+
elseif ($PSBoundParameters.ContainsKey('Path') -and $Path) {
73+
$target = [string]$Path
74+
}
75+
elseif ($args.Count -gt 0) {
76+
$target = [string]$args[0]
77+
}
78+
if ($target -eq 'C:\ProgramData\chocolatey') {
79+
return $false
80+
}
81+
return Microsoft.PowerShell.Management\Test-Path @PSBoundParameters
82+
} -ModuleName ChocolateyDetection
83+
}
84+
4585
AfterAll {
4686
Clear-ChocolateyTestEnvironment
4787
if ($script:TempDir -and (Test-Path -LiteralPath $script:TempDir)) {
@@ -99,6 +139,7 @@ Describe 'ChocolateyDetection extended scenarios' {
99139
$filePath = Join-Path $script:TempDir 'missing-choco.txt'
100140
New-Item -ItemType File -Path $filePath -Force | Out-Null
101141
Mock-EnvironmentVariable -Name 'ChocolateyInstall' -Value $filePath
142+
Disable-ChocolateyDefaultLocations
102143

103144
Get-ChocolateyRoot | Should -BeNullOrEmpty
104145
}
@@ -154,6 +195,7 @@ Describe 'ChocolateyDetection extended scenarios' {
154195

155196
It 'Logs verbose output when no Chocolatey root is found at debug level 2' {
156197
Mock-EnvironmentVariable -Name 'ChocolateyInstall' -Value $null
198+
Disable-ChocolateyDefaultLocations
157199
$env:PS_PROFILE_DEBUG = '2'
158200

159201
Get-ChocolateyRoot | Should -BeNullOrEmpty

tests/unit/library/chocolatey/library-chocolatey-detection.tests.ps1

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,47 @@ AfterAll {
3636
}
3737
}
3838

39+
function script:Disable-ChocolateyDefaultLocations {
40+
<#
41+
.SYNOPSIS
42+
Point ProgramData at an empty temp dir and block the hardcoded C:\ProgramData\chocolatey fallback.
43+
#>
44+
$emptyProgramData = Join-Path $script:TempDir 'empty-programdata'
45+
if (-not (Test-Path -LiteralPath $emptyProgramData)) {
46+
New-Item -ItemType Directory -Path $emptyProgramData -Force | Out-Null
47+
}
48+
Mock-EnvironmentVariable -Name 'ProgramData' -Value $emptyProgramData
49+
50+
# Prefer Test-Path fallbacks so the hardcoded path mock below is effective.
51+
Mock Get-Command {
52+
$cmdName = $Name
53+
if ([string]::IsNullOrWhiteSpace($cmdName) -and $args.Count -gt 0) {
54+
$cmdName = [string]$args[0]
55+
}
56+
if ($cmdName -eq 'Test-ValidPath') {
57+
return $null
58+
}
59+
return Microsoft.PowerShell.Core\Get-Command @PSBoundParameters
60+
} -ModuleName ChocolateyDetection
61+
62+
Mock Test-Path {
63+
$target = $null
64+
if ($PSBoundParameters.ContainsKey('LiteralPath') -and $LiteralPath) {
65+
$target = [string]$LiteralPath
66+
}
67+
elseif ($PSBoundParameters.ContainsKey('Path') -and $Path) {
68+
$target = [string]$Path
69+
}
70+
elseif ($args.Count -gt 0) {
71+
$target = [string]$args[0]
72+
}
73+
if ($target -eq 'C:\ProgramData\chocolatey') {
74+
return $false
75+
}
76+
return Microsoft.PowerShell.Management\Test-Path @PSBoundParameters
77+
} -ModuleName ChocolateyDetection
78+
}
79+
3980
Describe 'ChocolateyDetection Module' {
4081
Context 'Get-ChocolateyRoot' {
4182
AfterEach {
@@ -46,6 +87,7 @@ Describe 'ChocolateyDetection Module' {
4687

4788
It 'Returns null when no Chocolatey installation is present' {
4889
Mock-EnvironmentVariable -Name 'ChocolateyInstall' -Value $null
90+
Disable-ChocolateyDefaultLocations
4991
Get-ChocolateyRoot | Should -BeNullOrEmpty
5092
}
5193

@@ -72,6 +114,7 @@ Describe 'ChocolateyDetection Module' {
72114

73115
It 'Returns null when Chocolatey root cannot be determined' {
74116
Mock-EnvironmentVariable -Name 'ChocolateyInstall' -Value $null
117+
Disable-ChocolateyDefaultLocations
75118

76119
Get-ChocolateyLibPath | Should -BeNullOrEmpty
77120
Get-ChocolateyBinPath | Should -BeNullOrEmpty

tests/unit/library/command/library-command-extended.tests.ps1

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,25 @@ Describe 'Command extended scenarios' {
8686
}
8787

8888
It 'Selects the Linux install command from a platform map' {
89-
$installMap = @{
90-
Windows = 'winget install Example'
91-
Linux = 'apt-get install -y example'
92-
macOS = 'brew install example'
93-
}
89+
$previousPlatform = $env:PS_PROFILE_PLATFORM_FORCE_NAME
90+
$env:PS_PROFILE_PLATFORM_FORCE_NAME = 'Linux'
91+
try {
92+
$installMap = @{
93+
Windows = 'winget install Example'
94+
Linux = 'apt-get install -y example'
95+
macOS = 'brew install example'
96+
}
9497

95-
Resolve-InstallCommand -InstallCommand $installMap | Should -Be 'apt-get install -y example'
98+
Resolve-InstallCommand -InstallCommand $installMap | Should -Be 'apt-get install -y example'
99+
}
100+
finally {
101+
if ($null -eq $previousPlatform) {
102+
Remove-Item Env:PS_PROFILE_PLATFORM_FORCE_NAME -ErrorAction SilentlyContinue
103+
}
104+
else {
105+
$env:PS_PROFILE_PLATFORM_FORCE_NAME = $previousPlatform
106+
}
107+
}
96108
}
97109
}
98110

@@ -585,15 +597,26 @@ Describe 'Command extended scenarios' {
585597
ExternalTools = @{
586598
'demo-tool' = @{
587599
InstallCommand = @{
588-
Linux = 'apt install demo-tool'
600+
Windows = 'winget install demo-tool'
601+
Linux = 'apt install demo-tool'
589602
}
590603
}
591604
}
592605
}
593606
}
594607

608+
$expected = if ($IsWindows -or $PSVersionTable.PSVersion.Major -lt 6) {
609+
'Install with: winget install demo-tool'
610+
}
611+
elseif ($IsLinux) {
612+
'Install with: apt install demo-tool'
613+
}
614+
else {
615+
'Install with: scoop install demo-tool'
616+
}
617+
595618
Get-ToolInstallHint -ToolName 'demo-tool' -RepoRoot '/tmp/demo-repo' |
596-
Should -Be 'Install with: apt install demo-tool'
619+
Should -Be $expected
597620
}
598621

599622
It 'Returns default hint when resolved install command is empty' {

tests/unit/library/envfile/library-envfile-extended.tests.ps1

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,20 @@ QUOTED_DOUBLE="say \"hello\""
282282
$repoRoot = Join-Path $script:TempRoot 'walk-git-repo'
283283
$nestedDir = Join-Path $repoRoot 'nested' 'deep'
284284
New-Item -ItemType Directory -Path $nestedDir -Force | Out-Null
285-
New-Item -ItemType File -Path (Join-Path $repoRoot '.git') -Force | Out-Null
285+
New-Item -ItemType Directory -Path (Join-Path $repoRoot '.git') -Force | Out-Null
286286
'WALK_VAR=detected' | Set-Content -LiteralPath (Join-Path $repoRoot '.env') -Encoding UTF8
287287

288+
# Force the cwd walk path: ignore checkout Get-RepoRoot / $Profile resolution.
289+
function global:Get-RepoRoot {
290+
param([string]$ScriptPath)
291+
throw 'forced miss for Initialize-EnvFiles walk fixture'
292+
}
293+
$hadProfile = Test-Path Variable:\global:Profile
294+
$savedProfile = if ($hadProfile) { $global:Profile } else { $null }
295+
if ($hadProfile) {
296+
Remove-Variable -Name Profile -Scope Global -ErrorAction SilentlyContinue
297+
}
298+
288299
$previousLocation = Get-Location
289300
try {
290301
Set-Location -LiteralPath $nestedDir
@@ -294,6 +305,10 @@ QUOTED_DOUBLE="say \"hello\""
294305
finally {
295306
Set-Location -LiteralPath $previousLocation.Path
296307
Remove-Item Env:\WALK_VAR -ErrorAction SilentlyContinue
308+
Remove-TestFunction -Name 'Get-RepoRoot'
309+
if ($hadProfile) {
310+
Set-Variable -Name Profile -Scope Global -Value $savedProfile
311+
}
297312
}
298313
}
299314

tests/unit/library/file/library-file-content-extended.tests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Describe 'FileContent extended scenarios' {
3737
$file = Join-Path $script:TempRoot 'present.txt'
3838
Set-Content -LiteralPath $file -Value 'payload' -Encoding UTF8
3939

40-
Read-FileContentOrNull -Path $file | Should -Match '^payload$'
40+
Read-FileContentOrNull -Path $file | Should -Match '^payload\r?$'
4141
}
4242

4343
It 'Returns null for missing files' {

0 commit comments

Comments
 (0)