Skip to content

Commit 3389259

Browse files
committed
fix: restore mirrored Pester coverage to 99% target
Add and extend source-mirrored, dot-source Pester tests across src/private/{quality,package,release,scaffold,build,shared,cli} to close the gaps remaining after the test-layout migration (#200-#207) and bring measured line coverage from 96.37% back to 99.28%, above the configured Pester.CodeCoverage.Enabled gate of 99%.
1 parent 1eca818 commit 3389259

57 files changed

Lines changed: 2836 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
2828
- Added mirrored, dot-source-first Pester test files for release private helpers (`src/private/release/*.ps1`), so most release, publish, and version-update helpers (local publish path resolution, publish parameter mapping, version label inference, version part calculation, prerelease label handling, release request shaping, and per-step write/import helpers) now have a focused test file under `tests/private/release/`. Multi-step release/publish/version-update workflow orchestrators (`InvokeNovaReleaseWorkflow.ps1`, `InvokeNovaPublishWorkflow.ps1`, `GetNovaVersionUpdateWorkflowContext.ps1`) stay covered by the existing cross-cutting release tests.
2929
- Added mirrored, dot-source-first Pester test files for package private helpers (`src/private/package/*.ps1`), so most package metadata, archive-content, NuGet/Zip artifact, upload option, upload target, upload header/auth, upload file-resolution, upload request, and per-type upload helpers now have a focused test file under `tests/private/package/`. Multi-step package workflow orchestrators (`InvokeNovaPackageWorkflow.ps1`, `InvokeNovaPackageUploadWorkflow.ps1`, `GetNovaPackageWorkflowContext.ps1`, `GetNovaPackageUploadWorkflowContext.ps1`, `InvokeNovaPackageArtifactCreation.ps1`, `NewNovaPackageArtifacts.ps1`) stay covered by the existing cross-cutting package tests.
3030
- Added mirrored, dot-source-first Pester test files for every public command (`src/public/*.ps1`), so each command (`Invoke-NovaBuild`, `Test-NovaBuild`, `New-NovaModulePackage`, `Initialize-NovaModule`, `Get-NovaProjectInfo`, `Get-/Set-NovaUpdateNotificationPreference`, `Install-NovaCli`, `Invoke-NovaCli`, `Update-NovaModuleTool`, `Update-NovaModuleVersion`, `Deploy-NovaPackage`, `Publish-NovaModule`, `Invoke-NovaRelease`) now has a focused command-surface test under `tests/public/` covering parameter forwarding, delegation to its workflow context/workflow helpers, and `ShouldProcess` / `-WhatIf` behavior without duplicating private helper coverage.
31+
- Restored measured code coverage to the configured 99% target after the test-layout migration by adding mirrored, dot-source-first tests and branch coverage extensions across the remaining gap files in `src/private/quality/`, `src/private/package/`, `src/private/release/`, `src/private/scaffold/`, `src/private/build/`, and `src/private/shared/`. The full suite now reports 99% line coverage against `src/**/*.ps1` with no dist-folder dependency in any test file.
3132

3233

3334
### Changed
3435
- Retired the pre-migration broad coverage-bucket suites (`CoverageGaps*`, `CoverageCompletion*`, `Remaining*Coverage*`, broad `NovaCommandModel*`) by moving them to `tests/_legacy/` and renaming the `.Tests.ps1` suffix to `.LegacyReference.ps1` so Pester discovery no longer runs them. The mirrored `tests/public/` and `tests/private/<domain>/` suites added in #200#206 now own that coverage; the retired files stay readable as a reference until coverage measurement is re-enabled and confirms no regression.
3536
- Migrated the remaining mirrored private-helper tests (`tests/private/update/`, `tests/private/quality/`, `tests/private/build/InvokeNovaBuildWorkflow.Tests.ps1`, and `tests/private/scaffold/`) off the legacy `Import-Module dist + InModuleScope` pattern to the dot-source-first model, so the full mirrored test suite runs against `src/**/*.ps1` directly and does not require a prior `Invoke-NovaBuild`.
3637
- Retired the dist-requiring integration and guardrail tests (`Module`, `OutputFiles`, `BuildOptions`, `CiCoverage`, `CliHelperCoverage`, `CliSharedParser`, `PackageLatestPolicy`, `PreambleBuild`, `UpdateNotification`) by moving them to `tests/_legacy/` with the `.LegacyReference.ps1` suffix and moving their `*TestSupport.ps1` sidecars alongside them, so `nova test` no longer depends on `dist/NovaModuleTools` being built first. Their behavior is now covered by the mirrored suites for each touched helper.
3738
- Test-loading guidance for NovaModuleTools' own tests and for generated Agentic Copilot scaffolds now follows a source-mirrored, dot-source-first pattern: new mirrored tests dot-source the relevant `src/**/*.ps1` files directly in `BeforeAll` instead of importing the built `dist` module or wrapping assertions in `InModuleScope`, so tests run against source files and JaCoCo coverage references real source paths.
38-
- `project.json` `Pester.CodeCoverage.Enabled` is temporarily `false` while the existing test suite migrates to the new dot-source pattern; the configured `90` percent target stays in place and is re-enabled once migration completes.
39+
- `project.json` `Pester.CodeCoverage.Enabled` is `true` with a `99` percent target that the migrated, source-mirrored test suite now satisfies; this finalizes the temporary disable from the test-layout migration.
3940
- The architect/design flow now surfaces settled vs unresolved design items before finalization, offers explicit choices for full finalization vs design-package-only handoff, clarifies how to use design notes versus the paste-ready GitHub issue draft, and requires finalization output to follow the project Markdown authoring guidance.
4041
- `ProjectTemplate.json` and the packaged example `project.json` now include Nova's default Pester `CodeCoverage` block with `Enabled=false`, shared `src/` coverage paths, JaCoCo output, and a `90` percent target so new projects can opt into coverage without hand-authoring the configuration.
4142
- `Invoke-NovaModuleToolsCI.ps1` now delegates the full test run to a single `Test-NovaBuild` call; the previous second `Invoke-Pester` pass and post-run Cobertura source-path remapping step have been removed.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
BeforeAll {
2+
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
3+
. (Join-Path $projectRoot 'src/private/build/AssertNovaPublicFunctionFileLayout.ps1')
4+
5+
function Stop-NovaOperation {
6+
param([string]$Message, [string]$ErrorId, [System.Management.Automation.ErrorCategory]$Category, $TargetObject)
7+
$exception = [System.Exception]::new($Message)
8+
$record = [System.Management.Automation.ErrorRecord]::new($exception, $ErrorId, $Category, $TargetObject)
9+
throw $record
10+
}
11+
function Get-FunctionNameFromFile {param($filePath)}
12+
}
13+
14+
Describe 'Get-NovaPublicFunctionFileList' {
15+
It 'returns *.ps1 files from the public directory' {
16+
$tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
17+
New-Item -ItemType Directory -Path $tmp | Out-Null
18+
Set-Content -Path (Join-Path $tmp 'A.ps1') -Value 'function A {}'
19+
Set-Content -Path (Join-Path $tmp 'B.txt') -Value 'not ps'
20+
try {
21+
$files = @(Get-NovaPublicFunctionFileList -ProjectInfo ([pscustomobject]@{PublicDir = $tmp}))
22+
$files.Count | Should -Be 1
23+
$files[0].Name | Should -Be 'A.ps1'
24+
} finally {
25+
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
26+
}
27+
}
28+
29+
It 'returns empty when public dir does not exist' {
30+
$tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
31+
Get-NovaPublicFunctionFileList -ProjectInfo ([pscustomobject]@{PublicDir = $tmp}) | Should -BeNullOrEmpty
32+
}
33+
}
34+
35+
Describe 'Get-NovaInvalidPublicFunctionFileList' {
36+
BeforeEach {
37+
$script:tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
38+
New-Item -ItemType Directory -Path $script:tmp | Out-Null
39+
Set-Content -Path (Join-Path $script:tmp 'Single.ps1') -Value 'x'
40+
Set-Content -Path (Join-Path $script:tmp 'Double.ps1') -Value 'x'
41+
Set-Content -Path (Join-Path $script:tmp 'None.ps1') -Value 'x'
42+
}
43+
AfterEach {
44+
Remove-Item $script:tmp -Recurse -Force -ErrorAction SilentlyContinue
45+
}
46+
47+
It 'excludes files with exactly one top-level function' {
48+
Mock Get-FunctionNameFromFile {
49+
if ($filePath -like '*Single.ps1') { return @('Foo') }
50+
if ($filePath -like '*Double.ps1') { return @('A','B') }
51+
return @()
52+
}
53+
$invalid = Get-NovaInvalidPublicFunctionFileList -ProjectInfo ([pscustomobject]@{PublicDir=$script:tmp})
54+
($invalid.FilePath | ForEach-Object { Split-Path -Leaf $_ }) | Should -Contain 'Double.ps1'
55+
($invalid.FilePath | ForEach-Object { Split-Path -Leaf $_ }) | Should -Contain 'None.ps1'
56+
($invalid.FilePath | ForEach-Object { Split-Path -Leaf $_ }) | Should -Not -Contain 'Single.ps1'
57+
}
58+
59+
It 'returns relative paths when ProjectRoot is provided' {
60+
Mock Get-FunctionNameFromFile { @('A','B') }
61+
$invalid = Get-NovaInvalidPublicFunctionFileList -ProjectInfo ([pscustomobject]@{PublicDir=$script:tmp; ProjectRoot=$script:tmp})
62+
$invalid[0].FilePath | Should -Not -Match '^[/\\]'
63+
}
64+
}
65+
66+
Describe 'Format-NovaPublicFunctionFileValidationMessage' {
67+
It 'lists invalid files with their function names' {
68+
$list = @(
69+
[pscustomobject]@{FilePath='src/public/A.ps1'; FunctionNameList=@('A','B')}
70+
[pscustomobject]@{FilePath='src/public/Empty.ps1'; FunctionNameList=@()}
71+
)
72+
$msg = Format-NovaPublicFunctionFileValidationMessage -InvalidFileList $list
73+
$msg | Should -Match 'src/public/A\.ps1: A, B'
74+
$msg | Should -Match 'src/public/Empty\.ps1: <none>'
75+
$msg | Should -Match 'override-warning'
76+
}
77+
}
78+
79+
Describe 'Assert-NovaPublicFunctionFileLayout' {
80+
It 'returns silently when no invalid files' {
81+
Mock Get-NovaInvalidPublicFunctionFileList { @() }
82+
{ Assert-NovaPublicFunctionFileLayout -ProjectInfo ([pscustomobject]@{PublicDir='src/public'}) } | Should -Not -Throw
83+
}
84+
85+
It 'warns and stops when invalid files exist' {
86+
Mock Get-NovaInvalidPublicFunctionFileList { @([pscustomobject]@{FilePath='x.ps1'; FunctionNameList=@('A','B')}) }
87+
Mock Write-Warning {}
88+
{ Assert-NovaPublicFunctionFileLayout -ProjectInfo ([pscustomobject]@{PublicDir='src/public'}) } | Should -Throw -ErrorId 'Nova.Validation.PublicFunctionFileLayoutInvalid'
89+
Assert-MockCalled Write-Warning -Times 1
90+
}
91+
92+
It 'warns and continues when OverrideWarningRequested' {
93+
Mock Get-NovaInvalidPublicFunctionFileList { @([pscustomobject]@{FilePath='x.ps1'; FunctionNameList=@('A','B')}) }
94+
Mock Write-Warning {}
95+
{ Assert-NovaPublicFunctionFileLayout -ProjectInfo ([pscustomobject]@{PublicDir='src/public'}) -OverrideWarningRequested } | Should -Not -Throw
96+
Assert-MockCalled Write-Warning -Times 1
97+
}
98+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
BeforeAll {
2+
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
3+
. (Join-Path $projectRoot 'src/private/build/BuildHelp.ps1')
4+
5+
function Stop-NovaOperation {
6+
param([string]$Message, [string]$ErrorId, [System.Management.Automation.ErrorCategory]$Category, $TargetObject)
7+
$exception = [System.Exception]::new($Message)
8+
$record = [System.Management.Automation.ErrorRecord]::new($exception, $ErrorId, $Category, $TargetObject)
9+
throw $record
10+
}
11+
function Get-NovaBuildProjectInfo {param($ProjectInfo); return $ProjectInfo}
12+
function Get-NovaHelpLocale {param($HelpMarkdownFiles); return 'en-US'}
13+
function Measure-PlatyPSMarkdown {[CmdletBinding()] param([Parameter(ValueFromPipeline)]$Input) process {}}
14+
function Import-MarkdownCommandHelp {[CmdletBinding()] param([Parameter(ValueFromPipeline)]$Input, $Path) process {}}
15+
function Export-MamlCommandHelp {[CmdletBinding()] param([Parameter(ValueFromPipeline)]$Input, $OutputFolder) process {}}
16+
}
17+
18+
Describe 'Get-NovaHelpDocsDir' {
19+
It 'joins DocsDir and ProjectName' {
20+
$ctx = [pscustomobject]@{DocsDir = '/repo/docs'; ProjectName = 'MyMod'}
21+
Get-NovaHelpDocsDir -BuildProjectInfo $ctx | Should -Be ([IO.Path]::Combine('/repo/docs','MyMod'))
22+
}
23+
}
24+
25+
Describe 'Get-NovaHelpMarkdownItem' {
26+
It 'returns the markdown files in the help docs dir' {
27+
$tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
28+
$sub = Join-Path $tmp 'MyMod'
29+
New-Item -ItemType Directory -Path $sub -Force | Out-Null
30+
Set-Content -Path (Join-Path $sub 'X.md') -Value 'help'
31+
try {
32+
$ctx = [pscustomobject]@{DocsDir = $tmp; ProjectName = 'MyMod'}
33+
$files = @(Get-NovaHelpMarkdownItem -BuildProjectInfo $ctx)
34+
$files.Count | Should -Be 1
35+
$files[0].Name | Should -Be 'X.md'
36+
} finally {
37+
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
38+
}
39+
}
40+
41+
It 'returns nothing when help docs dir is missing' {
42+
$ctx = [pscustomobject]@{DocsDir = '/no/such/dir'; ProjectName = 'X'}
43+
Get-NovaHelpMarkdownItem -BuildProjectInfo $ctx | Should -BeNullOrEmpty
44+
}
45+
}
46+
47+
Describe 'Get-NovaHelpBuildContext' {
48+
BeforeEach {
49+
$script:tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
50+
New-Item -ItemType Directory -Path $script:tmp | Out-Null
51+
Set-Content -Path (Join-Path $script:tmp 'X.md') -Value 'help'
52+
$script:fakeFiles = @(Get-Item (Join-Path $script:tmp 'X.md'))
53+
}
54+
AfterEach { Remove-Item $script:tmp -Recurse -Force -ErrorAction SilentlyContinue }
55+
56+
It 'returns null when no PlatyPS command help files are detected' {
57+
$ctx = [pscustomobject]@{DocsDir='/x'; ProjectName='Y'}
58+
Mock Measure-PlatyPSMarkdown { @() }
59+
Get-NovaHelpBuildContext -BuildProjectInfo $ctx -HelpMarkdownFiles $script:fakeFiles | Should -BeNullOrEmpty
60+
}
61+
It 'returns a context with command help files when detected' {
62+
$ctx = [pscustomobject]@{DocsDir='/x'; ProjectName='Y'}
63+
Mock Measure-PlatyPSMarkdown { @([pscustomobject]@{FilePath='X.md'; FileType='CommandHelp'}) }
64+
Mock Get-NovaHelpLocale { 'en-US' }
65+
$result = Get-NovaHelpBuildContext -BuildProjectInfo $ctx -HelpMarkdownFiles $script:fakeFiles
66+
$result.Locale | Should -Be 'en-US'
67+
$result.CommandHelpFiles.Count | Should -Be 1
68+
}
69+
}
70+
71+
Describe 'Rename-NovaGeneratedHelpFolder' {
72+
It 'throws when expected generated dir is missing' {
73+
$tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
74+
New-Item -ItemType Directory -Path $tmp | Out-Null
75+
try {
76+
$ctx = [pscustomobject]@{OutputModuleDir=$tmp; ProjectName='X'; DocsDir=$tmp}
77+
{ Rename-NovaGeneratedHelpFolder -BuildProjectInfo $ctx -Locale 'en-US' } | Should -Throw -ErrorId 'Nova.Environment.GeneratedHelpDirectoryNotFound'
78+
} finally {
79+
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
80+
}
81+
}
82+
It 'renames the generated help dir to the locale' {
83+
$tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
84+
$generated = Join-Path $tmp 'X'
85+
New-Item -ItemType Directory -Path $generated -Force | Out-Null
86+
try {
87+
$ctx = [pscustomobject]@{OutputModuleDir=$tmp; ProjectName='X'; DocsDir=$tmp}
88+
Rename-NovaGeneratedHelpFolder -BuildProjectInfo $ctx -Locale 'en-US'
89+
Test-Path (Join-Path $tmp 'en-US') | Should -BeTrue
90+
} finally {
91+
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
92+
}
93+
}
94+
}
95+
96+
Describe 'Assert-NovaPlatyPSAvailable' {
97+
It 'returns silently when PlatyPS is available' {
98+
Mock Get-Module { @([pscustomobject]@{Name='Microsoft.PowerShell.PlatyPS'}) }
99+
{ Assert-NovaPlatyPSAvailable } | Should -Not -Throw
100+
}
101+
It 'stops when PlatyPS is unavailable' {
102+
Mock Get-Module { @() }
103+
{ Assert-NovaPlatyPSAvailable } | Should -Throw -ErrorId 'Nova.Dependency.BuildHelpDependencyMissing'
104+
}
105+
}
106+
107+
Describe 'Export-NovaGeneratedHelp' {
108+
It 'imports command help, exports maml to OutputModuleDir, and renames the generated folder to the locale' {
109+
$tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
110+
$generated = Join-Path $tmp 'Y'
111+
New-Item -ItemType Directory -Path $generated -Force | Out-Null
112+
try {
113+
$ctx = [pscustomobject]@{OutputModuleDir=$tmp; ProjectName='Y'; DocsDir=$tmp}
114+
$helpContext = [pscustomobject]@{
115+
HelpMarkdownFiles = @()
116+
CommandHelpFiles = @([pscustomobject]@{FilePath = (Join-Path $tmp 'X.md')})
117+
Locale = 'en-US'
118+
}
119+
Mock Import-MarkdownCommandHelp {[CmdletBinding()] param([Parameter(ValueFromPipeline)]$Input, $Path) process {$_}}
120+
Mock Export-MamlCommandHelp {[CmdletBinding()] param([Parameter(ValueFromPipeline)]$Input, $OutputFolder) process {$script:exportFolder = $OutputFolder}}
121+
122+
Export-NovaGeneratedHelp -BuildProjectInfo $ctx -HelpContext $helpContext
123+
124+
Should -Invoke Import-MarkdownCommandHelp -Times 1
125+
Should -Invoke Export-MamlCommandHelp -Times 1
126+
Test-Path (Join-Path $tmp 'en-US') | Should -BeTrue
127+
} finally {
128+
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
129+
}
130+
}
131+
}
132+
133+
Describe 'Build-Help' {
134+
It 'returns when no help markdown files exist' {
135+
Mock Get-NovaBuildProjectInfo { [pscustomobject]@{DocsDir='/x'; ProjectName='Y'; OutputModuleDir='/o'} }
136+
Mock Get-NovaHelpMarkdownItem { @() }
137+
Mock Assert-NovaPlatyPSAvailable {}
138+
Mock Get-NovaHelpBuildContext {}
139+
Mock Export-NovaGeneratedHelp {}
140+
{ Build-Help -ProjectInfo ([pscustomobject]@{}) } | Should -Not -Throw
141+
Assert-MockCalled Assert-NovaPlatyPSAvailable -Times 0
142+
}
143+
144+
It 'returns when no PlatyPS context is found' {
145+
$tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
146+
New-Item -ItemType Directory -Path $tmp | Out-Null
147+
Set-Content -Path (Join-Path $tmp 'X.md') -Value 'help'
148+
try {
149+
Mock Get-NovaBuildProjectInfo { [pscustomobject]@{DocsDir='/x'; ProjectName='Y'; OutputModuleDir='/o'} }
150+
Mock Get-NovaHelpMarkdownItem { @(Get-Item (Join-Path $tmp 'X.md')) }
151+
Mock Assert-NovaPlatyPSAvailable {}
152+
Mock Get-NovaHelpBuildContext { $null }
153+
Mock Export-NovaGeneratedHelp {}
154+
Build-Help -ProjectInfo ([pscustomobject]@{})
155+
Assert-MockCalled Export-NovaGeneratedHelp -Times 0
156+
} finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
157+
}
158+
159+
It 'exports help when PlatyPS context is found' {
160+
$tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
161+
New-Item -ItemType Directory -Path $tmp | Out-Null
162+
Set-Content -Path (Join-Path $tmp 'X.md') -Value 'help'
163+
try {
164+
Mock Get-NovaBuildProjectInfo { [pscustomobject]@{DocsDir='/x'; ProjectName='Y'; OutputModuleDir='/o'} }
165+
Mock Get-NovaHelpMarkdownItem { @(Get-Item (Join-Path $tmp 'X.md')) }
166+
Mock Assert-NovaPlatyPSAvailable {}
167+
Mock Get-NovaHelpBuildContext { [pscustomobject]@{HelpMarkdownFiles=@(); CommandHelpFiles=@(); Locale='en-US'} }
168+
Mock Export-NovaGeneratedHelp {}
169+
Build-Help -ProjectInfo ([pscustomobject]@{})
170+
Assert-MockCalled Export-NovaGeneratedHelp -Times 1
171+
} finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
172+
}
173+
}

0 commit comments

Comments
 (0)