Skip to content

Commit c9efaa5

Browse files
committed
fix(#260): enforce supported Pester version range and improve error handling
- `Invoke-NovaTest`, `Test-NovaBuild`, and `% nova test` now only use Pester versions from `5.7.1` to `5.10.0`, failing with a clear error for unsupported `Pester 6.x`. - Updated documentation to reflect the new Pester version requirements.
1 parent 5a5a56a commit c9efaa5

15 files changed

Lines changed: 604 additions & 93 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1515

1616
### Fixed
1717

18+
- `Invoke-NovaTest`, `Test-NovaBuild`, and `% nova test` now resolve and import a supported installed `Pester` version from `5.7.1` through `5.10.0` instead of using an unsupported `Pester 6.x` installation automatically.
19+
- Nova test workflows now fail early with a clear dependency error when only unsupported `Pester 6.x` versions are available.
20+
1821
### Security
1922

2023
## [3.3.0] - 2026-06-16

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,9 @@ Notes:
298298
- if `project.json` sets `Pester.CodeCoverage.CoveragePercentTarget`, `Invoke-NovaTest` fails when the measured coverage percentage is lower than that configured target
299299
- this repository currently enables coverage with a `99` percent target; the template and packaged example
300300
`project.json` files ship the same JaCoCo configuration shape with `Enabled=false` and a `90` percent opt-in target
301-
- make sure `Pester 5.7.1` is available before running `Invoke-NovaTest` or `Test-NovaBuild`
302-
- the published `NovaModuleTools` manifest also declares `Pester 5.7.1`, so installed end-user workflows can still resolve that dependency automatically
301+
- make sure a supported `Pester` version from `5.7.1` through `5.10.0` is available before running `Invoke-NovaTest` or `Test-NovaBuild`
302+
- Nova resolves and imports a compatible installed `Pester 5.x` version for the managed test workflow and does not run tests with `Pester 6.x`
303+
- the published `NovaModuleTools` manifest declares `Pester` with `ModuleVersion = 5.7.1` and `MaximumVersion = 5.10.0`, so installed end-user workflows can still resolve that supported dependency range automatically
303304

304305
### Create a package artifact
305306

@@ -640,7 +641,7 @@ Responsibilities currently covered by the release pipeline include:
640641
- publishing to PowerShell Gallery
641642
- preparing the next prerelease version on `develop`
642643

643-
The workflow now uses `KeepAChangelog` for changelog release moves, creates annotated git tags named directly from the release version, and bootstraps the local PSResourceGet repository store before calling `Publish-NovaModule`. The shared CI installer also installs `Pester 5.7.1` explicitly before it installs prerelease gallery modules so test workflows do not rely on transitive manifest dependency resolution.
644+
The workflow now uses `KeepAChangelog` for changelog release moves, creates annotated git tags named directly from the release version, and bootstraps the local PSResourceGet repository store before calling `Publish-NovaModule`. The shared CI installer also installs `Pester 5.7.1` explicitly before it installs prerelease gallery modules so test workflows stay on Nova's supported `Pester 5.x` range and do not rely on transitive manifest dependency resolution.
644645

645646
### Where NovaModuleTools cmdlets fit
646647

RELEASE_NOTE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang
1414

1515
### Fixed
1616

17+
- `Invoke-NovaTest`, `Test-NovaBuild`, and `% nova test` now stay on Nova's supported `Pester 5.x` range and stop with a clear error instead of trying to run with an installed `Pester 6.x` version.
18+
1719
### Security
1820

1921
## [3.3.0] - 2026-06-16

docs/NovaModuleTools/en-US/Invoke-NovaTest.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ PS> Invoke-NovaTest [[-TagFilter] <string[]>] [[-ExcludeTagFilter] <string[]>]
3232

3333
`Invoke-NovaTest` reads the Pester configuration from `project.json`, discovers the repository unit-test files, and runs the managed Nova test workflow without rebuilding the project first.
3434

35+
Nova resolves a supported installed `Pester` version from `5.7.1` through `5.10.0` for the managed test workflow and stops with a clear dependency error when only unsupported `Pester 6.x` versions are available.
36+
3537
The unit-test workflow writes NUnit XML to `artifacts/UnitTestResults.xml`.
3638

3739
When `Pester.CodeCoverage.Enabled` is `true`, Nova also writes JaCoCo coverage to `artifacts/coverage.xml` and fails the run when the measured percentage is lower than `Pester.CodeCoverage.CoveragePercentTarget`.

docs/NovaModuleTools/en-US/Test-NovaBuild.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ PS> Test-NovaBuild [[-TagFilter] <string[]>] [[-ExcludeTagFilter] <string[]>]
3131

3232
`Test-NovaBuild` reads the Pester configuration from `project.json`, discovers the build-validation integration tests for the current project, and runs the managed Nova test workflow against the built-module validation surface.
3333

34+
Nova resolves a supported installed `Pester` version from `5.7.1` through `5.10.0` for the managed test workflow and stops with a clear dependency error when only unsupported `Pester 6.x` versions are available.
35+
3436
This build-validation flow writes NUnit XML to `artifacts/TestResults.xml`.
3537

3638
Unlike `Invoke-NovaTest`, this command does not enforce source-coverage targets. Use `Invoke-NovaTest` for the unit-test and code-coverage workflow, and use `Test-NovaBuild` when you need build-validation integration coverage that reflects the built module path.
3739

38-
If the current project does not contain any `*.Integration.Tests.ps1` files, `Test-NovaBuild` stops with a Nova-native error that explains the expected naming and reminds you to use `Invoke-NovaTest` for unit tests.
40+
If the current project does not contain any `*.Integration.Tests.ps1` files, `Test-NovaBuild` writes an actionable warning that explains the expected naming and reminds you to use `Invoke-NovaTest` for unit tests.
3941

4042
`-OverrideWarning` lets the nested build-validation flow continue even if the `src/public` layout guard reports zero or multiple top-level functions in a public file.
4143

@@ -271,5 +273,5 @@ Returns the Pester result object from the managed build-validation run.
271273

272274
## RELATED LINKS
273275

274-
[Invoke-NovaTest](Invoke-NovaTest.md)
275-
[Invoke-NovaBuild](Invoke-NovaBuild.md)
276+
- [Invoke-NovaTest](./Invoke-NovaTest.md)
277+
- [Invoke-NovaBuild](./Invoke-NovaBuild.md)

src/private/quality/GetNovaTestWorkflowContext.ps1

Lines changed: 167 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,173 @@ function Get-NovaTestWorkflowOperation {
1212
}
1313

1414
function Assert-NovaPesterAvailable {
15+
[CmdletBinding()]
16+
param(
17+
[AllowNull()][pscustomobject]$ProjectInfo
18+
)
19+
20+
$moduleSpecification = Get-NovaSupportedPesterModuleSpecification -ProjectInfo $ProjectInfo
21+
Import-NovaSupportedPesterModule -ModuleSpecification $moduleSpecification
22+
return $moduleSpecification
23+
}
24+
25+
function Get-NovaSupportedPesterModuleSpecification {
26+
[CmdletBinding()]
27+
param(
28+
[AllowNull()][pscustomobject]$ProjectInfo
29+
)
30+
31+
$moduleRequirement = Get-NovaPesterModuleRequirement -ProjectInfo $ProjectInfo
32+
$availableModule = @(Get-AvailableNovaPesterModule -ModuleRequirement $moduleRequirement)
33+
if ($availableModule.Count -eq 0) {
34+
Stop-NovaOperation -Message (Get-NovaPesterDependencyMessage -ModuleRequirement $moduleRequirement) -ErrorId 'Nova.Dependency.PesterDependencyMissing' -Category ResourceUnavailable -TargetObject 'Pester'
35+
}
36+
37+
$selectedVersion = [version]$availableModule[0].Version
38+
return [pscustomobject]@{
39+
Name = 'Pester'
40+
MinimumVersion = $moduleRequirement.MinimumVersion
41+
MaximumVersion = $moduleRequirement.MaximumVersion
42+
SelectedVersion = $selectedVersion
43+
FullyQualifiedName = @{
44+
ModuleName = 'Pester'
45+
RequiredVersion = [string]$selectedVersion
46+
}
47+
}
48+
}
49+
50+
function Get-NovaSupportedPesterRange {
1551
[CmdletBinding()]
1652
param()
1753

18-
if (-not (Get-Module -Name Pester -ListAvailable)) {
19-
Stop-NovaOperation -Message 'The module Pester must be installed to run Nova tests. Install Pester 5.7.1 and try again.' -ErrorId 'Nova.Dependency.PesterDependencyMissing' -Category ResourceUnavailable -TargetObject 'Pester'
54+
return [pscustomobject]@{
55+
Name = 'Pester'
56+
MinimumVersion = [version]'5.7.1'
57+
MaximumVersion = [version]'5.10.0'
58+
}
59+
}
60+
61+
function Get-NovaPesterModuleRequirement {
62+
[CmdletBinding()]
63+
param(
64+
[AllowNull()][pscustomobject]$ProjectInfo
65+
)
66+
67+
$supportedRequirement = Get-NovaSupportedPesterRange
68+
$manifestSettings = Get-NovaPesterSettingValue -InputObject $ProjectInfo -Name 'Manifest'
69+
$requiredModules = @(Get-NovaPesterSettingValue -InputObject $manifestSettings -Name 'RequiredModules')
70+
$pesterModule = $requiredModules |
71+
Where-Object {(Get-NovaPesterSettingValue -InputObject $_ -Name 'ModuleName') -eq 'Pester'} |
72+
Select-Object -First 1
73+
74+
if ($null -ne $pesterModule) {
75+
$manifestRequirement = [pscustomobject]@{
76+
Name = 'Pester'
77+
MinimumVersion = [version](Get-NovaPesterVersionText -InputObject $pesterModule -Name 'ModuleVersion' -DefaultValue '5.7.1')
78+
MaximumVersion = [version](Get-NovaPesterVersionText -InputObject $pesterModule -Name 'MaximumVersion' -DefaultValue '5.10.0')
79+
}
80+
Assert-NovaPesterModuleRequirementSupported -ModuleRequirement $manifestRequirement -SupportedRequirement $supportedRequirement
2081
}
82+
83+
return $supportedRequirement
84+
}
85+
86+
function Assert-NovaPesterModuleRequirementSupported {
87+
[CmdletBinding()]
88+
param(
89+
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement,
90+
[Parameter(Mandatory)][pscustomobject]$SupportedRequirement
91+
)
92+
93+
if ($ModuleRequirement.MinimumVersion -eq $SupportedRequirement.MinimumVersion -and $ModuleRequirement.MaximumVersion -eq $SupportedRequirement.MaximumVersion) {
94+
return
95+
}
96+
97+
$supportedVersionText = Get-NovaPesterVersionRangeText -ModuleRequirement $SupportedRequirement
98+
$declaredVersionText = Get-NovaPesterVersionRangeText -ModuleRequirement $ModuleRequirement
99+
Stop-NovaOperation -Message "Nova tests support Pester only from $supportedVersionText. project.json declares Pester from $declaredVersionText. Update project.json to match Nova's supported range and try again." -ErrorId 'Nova.Dependency.UnsupportedPesterVersionRequirement' -Category InvalidData -TargetObject 'project.json'
100+
}
101+
102+
function Get-NovaPesterVersionText {
103+
[CmdletBinding()]
104+
param(
105+
[AllowNull()][object]$InputObject,
106+
[Parameter(Mandatory)][string]$Name,
107+
[Parameter(Mandatory)][string]$DefaultValue
108+
)
109+
110+
$value = [string](Get-NovaPesterSettingValue -InputObject $InputObject -Name $Name)
111+
if ( [string]::IsNullOrWhiteSpace($value)) {
112+
return $DefaultValue
113+
}
114+
115+
return $value
116+
}
117+
118+
function Get-NovaPesterVersionRangeText {
119+
[CmdletBinding()]
120+
param(
121+
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement
122+
)
123+
124+
return "$( $ModuleRequirement.MinimumVersion ) through $( $ModuleRequirement.MaximumVersion )"
125+
}
126+
127+
function Get-AvailableNovaPesterModule {
128+
[CmdletBinding()]
129+
param(
130+
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement
131+
)
132+
133+
return @(
134+
Get-Module -Name Pester -ListAvailable |
135+
Where-Object {Test-NovaPesterModuleVersionSupported -Version $_.Version -ModuleRequirement $ModuleRequirement} |
136+
Sort-Object Version -Descending
137+
)
138+
}
139+
140+
function Test-NovaPesterModuleVersionSupported {
141+
[CmdletBinding()]
142+
param(
143+
[AllowNull()][object]$Version,
144+
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement
145+
)
146+
147+
if ($null -eq $Version) {
148+
return $false
149+
}
150+
151+
$resolvedVersion = [version]$Version
152+
return $resolvedVersion -ge $ModuleRequirement.MinimumVersion -and $resolvedVersion -le $ModuleRequirement.MaximumVersion
153+
}
154+
155+
function Get-NovaPesterDependencyMessage {
156+
[CmdletBinding()]
157+
param(
158+
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement
159+
)
160+
161+
$supportedVersionText = Get-NovaPesterVersionRangeText -ModuleRequirement $ModuleRequirement
162+
$installedVersion = @(
163+
Get-Module -Name Pester -ListAvailable |
164+
Sort-Object Version -Descending |
165+
ForEach-Object {[string]$_.Version} |
166+
Select-Object -Unique
167+
)
168+
if ($installedVersion.Count -eq 0) {
169+
return "The module Pester must be installed to run Nova tests. Install a supported Pester version from $supportedVersionText and try again."
170+
}
171+
172+
return "Nova tests require Pester from $supportedVersionText. Installed versions: $( $installedVersion -join ', ' ). Install a supported Pester 5.x version and try again."
173+
}
174+
175+
function Import-NovaSupportedPesterModule {
176+
[CmdletBinding()]
177+
param(
178+
[Parameter(Mandatory)][pscustomobject]$ModuleSpecification
179+
)
180+
181+
Import-Module -FullyQualifiedName $ModuleSpecification.FullyQualifiedName -Force -ErrorAction Stop | Out-Null
21182
}
22183

23184
function Get-NovaTestWorkflowContext {
@@ -27,11 +188,12 @@ function Get-NovaTestWorkflowContext {
27188
[Parameter(Mandatory)][hashtable]$BoundParameters
28189
)
29190

30-
Test-ProjectSchema | Out-Null
31-
Assert-NovaPesterAvailable
32191
$projectInfo = Get-NovaProjectInfo
192+
Test-ProjectSchema | Out-Null
193+
$pesterModuleSpecification = Assert-NovaPesterAvailable -ProjectInfo $projectInfo
33194
$workflowProfile = Get-NovaTestWorkflowProfile -TestOption $TestOption
34195
$pesterConfig = New-PesterConfiguration -Hashtable $projectInfo.Pester
196+
Add-Member -InputObject $pesterConfig -MemberType NoteProperty -Name PesterModuleSpecification -Value $pesterModuleSpecification -Force
35197
$coverageConfiguration = Get-NovaPesterCoverageConfigurationState -ProjectInfo $projectInfo -CoverageEnabled:$workflowProfile.CoverageEnabled
36198
$pesterConfig.CodeCoverage.Enabled = $coverageConfiguration.Enabled
37199
$pesterConfig.CodeCoverage.Path = $coverageConfiguration.Path
@@ -62,6 +224,7 @@ function Get-NovaTestWorkflowContext {
62224
CommandName = $workflowProfile.CommandName
63225
OverrideWarningRequested = $BoundParameters.ContainsKey('OverrideWarning') -and [bool]$BoundParameters.OverrideWarning
64226
ProjectInfo = $projectInfo
227+
PesterModuleSpecification = $pesterModuleSpecification
65228
PesterSettings = Get-NovaTestWorkflowPesterConfiguration -ProjectPesterSettings $projectInfo.Pester -CoverageEnabled:$workflowProfile.CoverageEnabled
66229
PesterConfig = $pesterConfig
67230
TestResultPath = $testResultPath

src/private/quality/InvokeNovaTestWorkflow.ps1

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ function Invoke-NovaPesterWithSuppressedProgress {
153153
$heartbeatMilliseconds = 2000
154154
}
155155

156-
$execution = Get-NovaPesterExecution -Configuration $Configuration
156+
$moduleSpecification = Get-NovaPropertyValue -InputObject $Configuration -Name 'PesterModuleSpecification'
157+
$execution = Get-NovaPesterExecution -Configuration $Configuration -ModuleSpecification $moduleSpecification
157158
try {
158159
Write-NovaTestWorkflowPesterProgress -Execution $execution -ProgressContext $ProgressContext
159160
while (-not (Wait-NovaPesterExecution -Execution $execution -TimeoutMilliseconds $HeartbeatMilliseconds)) {
@@ -323,13 +324,19 @@ function Write-NovaPesterHostInformationMessage {
323324
function Get-NovaPesterExecution {
324325
[CmdletBinding()]
325326
param(
326-
[Parameter(Mandatory)][object]$Configuration
327+
[Parameter(Mandatory)][object]$Configuration,
328+
[AllowNull()][object]$ModuleSpecification
327329
)
328330

329331
$powershell = [powershell]::Create()
330332
$command = @'
331-
param($Configuration)
332-
Import-Module Pester -ErrorAction Stop
333+
param($Configuration, $ModuleSpecification)
334+
if ($null -ne $ModuleSpecification) {
335+
Import-Module -FullyQualifiedName $ModuleSpecification -Force -ErrorAction Stop
336+
}
337+
else {
338+
Import-Module Pester -ErrorAction Stop
339+
}
333340
$previousProgressPreference = $global:ProgressPreference
334341
$global:ProgressPreference = 'SilentlyContinue'
335342
try {
@@ -338,7 +345,12 @@ try {
338345
$global:ProgressPreference = $previousProgressPreference
339346
}
340347
'@
341-
$null = $powershell.AddScript($command).AddArgument($Configuration)
348+
$moduleImportSpecification = $null
349+
if ($null -ne $ModuleSpecification) {
350+
$moduleImportSpecification = Get-NovaPropertyValue -InputObject $ModuleSpecification -Name 'FullyQualifiedName'
351+
}
352+
353+
$null = $powershell.AddScript($command).AddArgument($Configuration).AddArgument($moduleImportSpecification)
342354

343355
return [pscustomobject]@{
344356
PowerShell = $powershell

tests/RunQualityLoop.Tests.ps1

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
BeforeAll {
2+
$script:repoRoot = Split-Path -Parent $PSScriptRoot
3+
$script:runScriptPath = Join-Path $script:repoRoot 'run.ps1'
4+
}
5+
6+
Describe 'run.ps1 quality loop' {
7+
It 'runs Invoke-NovaTest and Test-NovaBuild through fresh child pwsh sessions' {
8+
$content = Get-Content -LiteralPath $script:runScriptPath -Raw
9+
10+
$content.Contains('function Invoke-NovaFreshValidationCommand') | Should -BeTrue
11+
$content.Contains('& pwsh -NoLogo -NoProfile -Command $Command') | Should -BeTrue
12+
$content.Contains('Invoke-NovaFreshValidationCommand -Command "Import-Module ''$builtModulePath'' -Force -ErrorAction Stop; Invoke-NovaTest"') | Should -BeTrue
13+
$content.Contains('Invoke-NovaFreshValidationCommand -Command "Import-Module ''$builtModulePath'' -Force -ErrorAction Stop; Test-NovaBuild"') | Should -BeTrue
14+
}
15+
}
16+
17+

tests/TestHelpers/PublicCommandIntegration.ps1

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,55 @@ function Invoke-NovaPublicCommandIntegrationInProjectRoot {
4646

4747
return Invoke-NovaPublicCommandIntegrationInLocation -Path $ProjectRoot -ScriptBlock $ScriptBlock
4848
}
49+
50+
function Invoke-NovaPublicCommandIntegrationInIsolatedSession {
51+
[CmdletBinding()]
52+
param(
53+
[Parameter(Mandatory)][string]$ProjectRoot,
54+
[string]$Path = $ProjectRoot,
55+
[Parameter(Mandatory)][scriptblock]$ScriptBlock
56+
)
57+
58+
$runnerPath = Join-Path $TestDrive 'Run-NovaPublicCommandIntegration.ps1'
59+
$moduleManifestPath = Join-Path $ProjectRoot 'dist/NovaModuleTools/NovaModuleTools.psd1'
60+
$escapedManifestPath = $moduleManifestPath.Replace("'", "''")
61+
$escapedLocationPath = $Path.Replace("'", "''")
62+
$runnerContent = @"
63+
function New-NovaPublicCommandIntegrationPesterModule {
64+
param(
65+
[Parameter(Mandatory)][string]`$BasePath,
66+
[Parameter(Mandatory)][string]`$Version
67+
)
68+
69+
`$moduleRoot = Join-Path `$BasePath "Pester/`$Version"
70+
`$null = New-Item -ItemType Directory -Path `$moduleRoot -Force
71+
Set-Content -LiteralPath (Join-Path `$moduleRoot 'Pester.psm1') -Value '' -Encoding utf8
72+
`$manifestContent = @(
73+
'@{'
74+
" RootModule = 'Pester.psm1'"
75+
" ModuleVersion = '`$Version'"
76+
" GUID = '`$([guid]::NewGuid().Guid)'"
77+
' FunctionsToExport = @()'
78+
' CmdletsToExport = @()'
79+
' VariablesToExport = @()'
80+
' AliasesToExport = @()'
81+
'}'
82+
) -join [Environment]::NewLine
83+
Set-Content -LiteralPath (Join-Path `$moduleRoot 'Pester.psd1') -Value `$manifestContent -Encoding utf8
84+
}
85+
86+
Import-Module '$escapedManifestPath' -Force -ErrorAction Stop
87+
Set-Location -LiteralPath '$escapedLocationPath'
88+
& {
89+
$($ScriptBlock.ToString() )
90+
}
91+
"@
92+
Set-Content -LiteralPath $runnerPath -Value $runnerContent -Encoding utf8
93+
94+
$output = & pwsh -NoLogo -NoProfile -File $runnerPath 2>&1
95+
return [pscustomobject]@{
96+
ExitCode = $LASTEXITCODE
97+
Output = @($output)
98+
}
99+
}
100+

0 commit comments

Comments
 (0)