Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Fixed

- `Invoke-NovaTest` now resolves Pester coverage paths and coverage-report output against the selected Nova project root instead of the PowerShell process's original working directory.
- Unit-test runs now keep coverage measurement, coverage thresholds, and generated test artifacts aligned with the project you selected through `Set-Location`, including when you switch between sibling projects in one PowerShell session.

### Security

## [3.4.0] - 2026-10-07
Expand All @@ -28,10 +31,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Fixed

- `Update-NovaModuleTool` and `% nova update` now split self-update failure details and recovery guidance across separate terminal lines, so long dependency errors stay readable instead of wrapping mid-sentence.
- `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.
- Nova test workflows now fail early with a clear dependency error when only unsupported `Pester 6.x` versions are available.
- Nova test workflows now reuse an already loaded supported `Pester 5.x` version in the current PowerShell session before selecting a different installed version.
- Repository CI no longer trips the `Pester` assembly loader by importing a gallery-installed `NovaModuleTools` module, building the local module, and then switching to another supported `Pester` version in the same session.

## [3.3.1] - 2026-07-15

Expand Down Expand Up @@ -552,4 +551,3 @@ This release was yanked because it removed the implicit `Pester` dependency, bef
[0.0.6]: https://github.com/stiwicourage/NovaModuleTools/compare/Version_0.0.5...Version_0.0.6
[0.0.5]: https://github.com/stiwicourage/NovaModuleTools/compare/Version_0.0.4...Version_0.0.5
[0.0.4]: https://github.com/stiwicourage/NovaModuleTools/compare/Version_0.0.3...Version_0.0.4

6 changes: 3 additions & 3 deletions RELEASE_NOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang

### Fixed

- `Invoke-NovaTest` now keeps coverage measurement, coverage thresholds, and generated test artifacts aligned with the selected project root even when you switch between sibling projects with `Set-Location` in the same PowerShell session.

### Security

## [3.4.0] - 2026-10-07
Expand All @@ -27,7 +29,6 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang
### Fixed

- `Update-NovaModuleTool` and `% nova update` now keep self-update failure details and recovery guidance on separate terminal lines so the error is easier to scan.
- `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.

## [3.3.1] - 2026-07-15

Expand Down Expand Up @@ -108,7 +109,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang

### Fixed

- PowerShell command help `RELATED LINKS` now use valid help-topic.
- PowerShell command help `RELATED LINKS` now use valid help-topic links instead of broken or non-help targets.

## [3.0.1] - 2026-05-18

Expand Down Expand Up @@ -289,4 +290,3 @@ This release was yanked because it removed the implicit `Pester` dependency befo
## [0.0.4] - 2024-06-25
### Added
- First PowerShell Gallery release of NovaModuleTools with the initial module workflow support.

2 changes: 2 additions & 0 deletions docs/NovaModuleTools/en-US/Invoke-NovaTest.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ The unit-test workflow writes NUnit XML to `artifacts/UnitTestResults.xml`.

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`.

Nova resolves test discovery, coverage inputs, and generated test artifacts from the selected project root for the current invocation, so `Set-Location` between sibling projects does not require a new PowerShell process.

Use `Test-NovaBuild` when you need the separate build-validation integration flow that runs against the built module output.

Use `-PesterConfigurationOverride` only when you need runtime-only unit-test data injection through file-backed `New-PesterContainer -Path` objects. In v1, Nova accepts only `Run.Container` and still keeps test discovery, output files, coverage, and required execution flags under Nova control.
Expand Down
2 changes: 1 addition & 1 deletion project.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"ProjectName": "NovaModuleTools",
"Description": "NovaModuleTools is an enterprise-focused build tool for PowerShell module development, with a strong emphasis on structure, maintainability, and build for automated CI/CD pipelines.",
"Version": "3.4.0",
"Version": "3.4.1-preview",
"Preamble": [
"Set-StrictMode -Version Latest",
"$ErrorActionPreference = 'Stop'"
Expand Down
44 changes: 41 additions & 3 deletions src/private/quality/GetNovaTestWorkflowContext.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ function Get-NovaTestWorkflowContext {
$coverageConfiguration = Get-NovaPesterCoverageConfigurationState -ProjectInfo $projectInfo -CoverageEnabled:$workflowProfile.CoverageEnabled
$pesterConfig.CodeCoverage.Enabled = $coverageConfiguration.Enabled
$pesterConfig.CodeCoverage.Path = $coverageConfiguration.Path
$pesterConfig.CodeCoverage.OutputPath = Resolve-NovaPesterProjectPath -ProjectRoot $projectInfo.ProjectRoot -Path (Get-NovaPesterPathOptionValue -InputObject (Get-NovaPesterSettingValue -InputObject $pesterConfig.CodeCoverage -Name 'OutputPath'))
if ($null -ne $coverageConfiguration.CoveragePercentTarget) {
$pesterConfig.CodeCoverage.CoveragePercentTarget = $coverageConfiguration.CoveragePercentTarget
}
Expand Down Expand Up @@ -469,7 +470,7 @@ function Get-NovaPesterCoverageFile {
[Parameter(Mandatory)][string]$ProjectRoot
)

$resolvedProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot -ErrorAction Stop).Path
$resolvedProjectRoot = [System.IO.Path]::GetFullPath($ProjectRoot)
foreach ($file in (Get-ChildItem -LiteralPath $resolvedProjectRoot -Recurse -File | Sort-Object FullName)) {
[pscustomobject]@{
FullPath = ConvertTo-NovaCoveragePathString -Path $file.FullName
Expand All @@ -489,12 +490,49 @@ function Add-NovaResolvedCoveragePath {
$patternVariant = @(Get-NovaCoveragePathPatternVariant -Pattern $Pattern)

foreach ($file in $CoverageFile) {
if ((Test-NovaCoveragePathMatch -CoverageFile $file -Pattern $patternVariant) -and -not $ResolvedPath.Contains($file.RelativePath)) {
$ResolvedPath.Add($file.RelativePath)
if ((Test-NovaCoveragePathMatch -CoverageFile $file -Pattern $patternVariant) -and -not $ResolvedPath.Contains($file.FullPath)) {
$ResolvedPath.Add($file.FullPath)
}
}
}

function Resolve-NovaPesterProjectPath {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$ProjectRoot,
[AllowNull()][string]$Path
)

if ([string]::IsNullOrWhiteSpace($Path)) {
return $null
}

$resolvedProjectRoot = [System.IO.Path]::GetFullPath($ProjectRoot)
if ([System.IO.Path]::IsPathRooted($Path)) {
return [System.IO.Path]::GetFullPath($Path)
}

return [System.IO.Path]::GetFullPath($Path, $resolvedProjectRoot)
}

function Get-NovaPesterPathOptionValue {
[CmdletBinding()]
param(
[AllowNull()][object]$InputObject
)

if ($null -eq $InputObject) {
return $null
}

$value = Get-NovaPesterSettingValue -InputObject $InputObject -Name 'Value'
if ($null -ne $value) {
return [string]$value
}

return [string]$InputObject
}

function Test-NovaCoveragePathMatch {
[CmdletBinding()]
param(
Expand Down
58 changes: 37 additions & 21 deletions src/private/quality/InvokeNovaTestWorkflow.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function Invoke-NovaTestWorkflowExecution {
StartPercentComplete = 70
EndPercentComplete = 94
}
$testResult = Invoke-NovaPesterWithSuppressedProgress -Configuration $WorkflowContext.PesterConfig -ProgressContext $testProgressContext
$testResult = Invoke-NovaPesterWithSuppressedProgress -Configuration $WorkflowContext.PesterConfig -ProgressContext $testProgressContext -ProjectRoot $WorkflowContext.ProjectInfo.ProjectRoot

Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Writing the test result report' -PercentComplete 96 -Action {
& $WorkflowContext.TestResultArtifactWriter.ScriptBlock -TestResult $testResult -OutputPath $WorkflowContext.TestResultPath -ReportWriter $WorkflowContext.TestResultReportWriter.ScriptBlock
Expand Down Expand Up @@ -145,7 +145,8 @@ function Invoke-NovaPesterWithSuppressedProgress {
[CmdletBinding()]
param(
[Parameter(Mandatory)][object]$Configuration,
[Parameter(Mandatory)][pscustomobject]$ProgressContext
[Parameter(Mandatory)][pscustomobject]$ProgressContext,
[string]$ProjectRoot
)

$heartbeatMilliseconds = Get-NovaPropertyValue -InputObject $ProgressContext -Name 'HeartbeatMilliseconds'
Expand All @@ -154,7 +155,7 @@ function Invoke-NovaPesterWithSuppressedProgress {
}

$moduleSpecification = Get-NovaPropertyValue -InputObject $Configuration -Name 'PesterModuleSpecification'
$execution = Get-NovaPesterExecution -Configuration $Configuration -ModuleSpecification $moduleSpecification
$execution = Get-NovaPesterExecution -Configuration $Configuration -ModuleSpecification $moduleSpecification -ProjectRoot $ProjectRoot
try {
Write-NovaTestWorkflowPesterProgress -Execution $execution -ProgressContext $ProgressContext
while (-not (Wait-NovaPesterExecution -Execution $execution -TimeoutMilliseconds $HeartbeatMilliseconds)) {
Expand Down Expand Up @@ -325,32 +326,18 @@ function Get-NovaPesterExecution {
[CmdletBinding()]
param(
[Parameter(Mandatory)][object]$Configuration,
[AllowNull()][object]$ModuleSpecification
[AllowNull()][object]$ModuleSpecification,
[string]$ProjectRoot
)

$powershell = [powershell]::Create()
$command = @'
param($Configuration, $ModuleSpecification)
if ($null -ne $ModuleSpecification) {
Import-Module -FullyQualifiedName $ModuleSpecification -Force -ErrorAction Stop
}
else {
Import-Module Pester -ErrorAction Stop
}
$previousProgressPreference = $global:ProgressPreference
$global:ProgressPreference = 'SilentlyContinue'
try {
Invoke-Pester -Configuration $Configuration
} finally {
$global:ProgressPreference = $previousProgressPreference
}
'@
$command = Get-NovaPesterExecutionScript
$moduleImportSpecification = $null
if ($null -ne $ModuleSpecification) {
$moduleImportSpecification = Get-NovaPropertyValue -InputObject $ModuleSpecification -Name 'FullyQualifiedName'
}

$null = $powershell.AddScript($command).AddArgument($Configuration).AddArgument($moduleImportSpecification)
$null = $powershell.AddScript($command).AddArgument($Configuration).AddArgument($moduleImportSpecification).AddArgument($ProjectRoot)

return [pscustomobject]@{
PowerShell = $powershell
Expand All @@ -363,6 +350,35 @@ try {
}
}

function Get-NovaPesterExecutionScript {
[CmdletBinding()]
param()

return @'
param($Configuration, $ModuleSpecification, $ProjectRoot)
if ($null -ne $ModuleSpecification) {
Import-Module -FullyQualifiedName $ModuleSpecification -Force -ErrorAction Stop
}
else {
Import-Module Pester -ErrorAction Stop
}
$enteredProjectRoot = -not [string]::IsNullOrWhiteSpace($ProjectRoot)
if ($enteredProjectRoot) {
Push-Location -LiteralPath $ProjectRoot
}
$previousProgressPreference = $global:ProgressPreference
$global:ProgressPreference = 'SilentlyContinue'
try {
Invoke-Pester -Configuration $Configuration
} finally {
$global:ProgressPreference = $previousProgressPreference
if ($enteredProjectRoot) {
Pop-Location
}
}
'@
}

function Wait-NovaPesterExecution {
[CmdletBinding()]
param(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function Get-NovaPesterRunPath {
ExcludePattern = $ExcludePattern
}

return @('tests/Example.Tests.ps1')
return @(Join-Path $ProjectInfo.ProjectRoot 'tests/Example.Tests.ps1')
}
function Get-NovaPesterTestResultPath {
param($ProjectRoot, $FileName)
Expand Down Expand Up @@ -63,7 +63,12 @@ function New-TestPesterConfig {
Filter = [pscustomobject]@{Tag = @(); ExcludeTag = @()}
Output = [pscustomobject]@{Verbosity = 'Detailed'; RenderMode = 'Auto'}
TestResult = [pscustomobject]@{Enabled = $true; OutputPath = $null}
CodeCoverage = [pscustomobject]@{Enabled = $true; CoveragePercentTarget = 80; Path = $null}
CodeCoverage = [pscustomobject]@{
Enabled = $true
CoveragePercentTarget = 80
OutputPath = [pscustomobject]@{Value = 'artifacts/coverage.xml'}
Path = $null
}
}
}

Expand Down
39 changes: 32 additions & 7 deletions tests/private/quality/GetNovaTestWorkflowContext.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Describe 'Get-NovaTestWorkflowContext' {
$result.BuildRequested | Should -BeFalse
$result.CommandName | Should -Be 'Invoke-NovaTest'
$result.PesterConfig.CodeCoverage.CoveragePercentTarget | Should -Be 99
$result.PesterConfig.CodeCoverage.OutputPath | Should -Be ([System.IO.Path]::Join($projectInfo.ProjectRoot, 'artifacts/coverage.xml'))
$result.PesterConfig.PesterModuleSpecification.SelectedVersion | Should -Be ([version]'5.10.0')
$result.PesterModuleSpecification.SelectedVersion | Should -Be ([version]'5.10.0')
$result.PesterSettings.CodeCoverage.Enabled | Should -BeTrue
Expand Down Expand Up @@ -146,7 +147,7 @@ Describe 'Get-NovaTestWorkflowContext' {
$result.TestDiscoveryMessageLines[3] | Should -Be 'Use Invoke-NovaTest for unit tests and Test-NovaBuild for build-validation integration tests.'
}

It 'expands configured coverage paths into concrete project-relative source files' {
It 'expands configured coverage paths into concrete source files rooted at the selected project' {
$projectRoot = Join-Path $TestDrive 'coverage-project'
foreach ($relativePath in @(
'src/public/GetAlpha.ps1'
Expand Down Expand Up @@ -178,12 +179,13 @@ Describe 'Get-NovaTestWorkflowContext' {
$result = Get-NovaTestWorkflowContext -TestOption @{TestMode = 'Unit'} -BoundParameters @{}

$result.PesterConfig.CodeCoverage.Path | Should -Be @(
'src/public/GetAlpha.ps1'
'src/private/GetBeta.ps1'
'src/private/quality/duplicates/GetDelta.ps1'
'src/private/quality/GetGamma.ps1'
'src/classes/NovaThing.ps1'
(Join-Path $projectRoot 'src/public/GetAlpha.ps1')
(Join-Path $projectRoot 'src/private/GetBeta.ps1')
(Join-Path $projectRoot 'src/private/quality/duplicates/GetDelta.ps1')
(Join-Path $projectRoot 'src/private/quality/GetGamma.ps1')
(Join-Path $projectRoot 'src/classes/NovaThing.ps1')
)
$result.PesterConfig.CodeCoverage.OutputPath | Should -Be ([System.IO.Path]::Join($projectRoot, 'artifacts/coverage.xml'))
}

It 'forwards the guarded Pester configuration override to the execution configuration initializer' {
Expand Down Expand Up @@ -275,6 +277,29 @@ Describe 'Get-NovaDiscoveredTestPathState' {
}
}

Describe 'Resolve-NovaPesterProjectPath' {
It 'returns null when the requested path is blank' {
Resolve-NovaPesterProjectPath -ProjectRoot $TestDrive -Path $null | Should -BeNullOrEmpty
Resolve-NovaPesterProjectPath -ProjectRoot $TestDrive -Path ' ' | Should -BeNullOrEmpty
}

It 'returns an absolute path unchanged when the requested path is already rooted' {
$absolutePath = [System.IO.Path]::GetFullPath((Join-Path $TestDrive 'artifacts/coverage.xml'))

Resolve-NovaPesterProjectPath -ProjectRoot (Join-Path $TestDrive 'project-root') -Path $absolutePath | Should -Be $absolutePath
}
}

Describe 'Get-NovaPesterPathOptionValue' {
It 'returns null when no option object was provided' {
Get-NovaPesterPathOptionValue -InputObject $null | Should -BeNullOrEmpty
}

It 'falls back to the input string when the object has no Value property' {
Get-NovaPesterPathOptionValue -InputObject 'artifacts/coverage.xml' | Should -Be 'artifacts/coverage.xml'
}
}

Describe 'Assert-NovaPesterAvailable' {
BeforeEach {
$script:lastImportModuleRequest = $null
Expand Down Expand Up @@ -558,7 +583,7 @@ Describe 'Get-NovaPesterCoverageConfigurationState' {

$result.Enabled | Should -BeTrue
$result.CoveragePercentTarget | Should -Be 99.0
$result.Path | Should -Be @('src/public/GetAlpha.ps1')
$result.Path | Should -Be @((Join-Path $projectRoot 'src/public/GetAlpha.ps1'))
}
}

Expand Down
5 changes: 4 additions & 1 deletion tests/private/quality/InvokeNovaTestWorkflow.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Describe 'Invoke-NovaTestWorkflow' {
It 'uses the pre-resolved coverage assertion after the Pester run' {
$global:coverageAssertionRan = $false
$workflowContext = [pscustomobject]@{
ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'; Pester = [ordered]@{}}
ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'; ProjectRoot = '/tmp/nova-project'; Pester = [ordered]@{}}
TestResultDirectory = '/tmp/nova-project/artifacts'
CommandName = 'Invoke-NovaTest'
TestResultPath = '/tmp/nova-project/artifacts/UnitTestResults.xml'
Expand Down Expand Up @@ -45,6 +45,9 @@ Describe 'Invoke-NovaTestWorkflow' {
$Status -eq 'Checking the configured code coverage target' -and $PercentComplete -eq 99
}
Assert-MockCalled Write-Progress -Times 1 -ParameterFilter {$Completed}
Assert-MockCalled Invoke-NovaPesterWithSuppressedProgress -Times 1 -ParameterFilter {
$ProjectRoot -eq '/tmp/nova-project'
}
Assert-MockCalled Write-Message -Times 1 -ParameterFilter {
$Text -eq 'Pester tests passed for NovaModuleTools' -and $color -eq 'Green'
}
Expand Down
Loading
Loading