Skip to content

Commit 82b00a2

Browse files
committed
fix: restore CodeScene-safe mirrored test health
Refactor the migrated mirrored Pester suite into per-file `*.TestSupport.ps1` sidecars and data-driven assertions so the current branch change set returns to Code Health 10 without reintroducing legacy coverage buckets or dist-module test loading. Also harden `Invoke-CodeSceneAnalysis.ps1` so JaCoCo path normalization and conditional branch-coverage upload stay maintainable and CI-safe.
1 parent 52c6038 commit 82b00a2

162 files changed

Lines changed: 1010 additions & 847 deletions

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
@@ -32,11 +32,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
3232

3333

3434
### Changed
35-
- Removed the pre-migration broad coverage-bucket suites (`CoverageGaps*`, `CoverageCompletion*`, `Remaining*Coverage*`, broad `NovaCommandModel*`). The mirrored `tests/public/` and `tests/private/<domain>/` suites added in #200#206 now own that coverage, and the restored 99% measured coverage confirms no regression, so the parked `tests/_legacy/` reference copies are no longer needed.
35+
- Removed the pre-migration broad coverage-bucket suites (`CoverageGaps*`, `CoverageCompletion*`, `Remaining*Coverage*`, broad `NovaCommandModel*`). The mirrored `tests/public/` and `tests/private/<domain>/` suites added in now own that coverage, and the restored 99% measured coverage confirms no regression, so the parked `tests/_legacy/` reference copies are no longer needed.
3636
- 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`.
3737
- Removed the dist-requiring integration and guardrail tests (`Module`, `OutputFiles`, `BuildOptions`, `CiCoverage`, `CliHelperCoverage`, `CliSharedParser`, `PackageLatestPolicy`, `PreambleBuild`, `UpdateNotification`) and their `*TestSupport.ps1` sidecars now that the mirrored suites cover the same behavior and `nova test` no longer depends on `dist/NovaModuleTools` being built first.
3838
- 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.
3939
- `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.
40+
- Refactored the current branch's source-mirrored test batch into per-file `*.TestSupport.ps1` sidecars and data-driven assertions where needed, so the migrated tests keep Code Health at 10 without falling back to broad legacy buckets or dist-module loading.
4041
- 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.
4142
- `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.
4243
- `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.

scripts/build/ci/Invoke-CodeSceneAnalysis.ps1

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -107,30 +107,43 @@ function Repair-PesterJaCoCoCoveragePath {
107107
param([Parameter(Mandatory)][string]$Path)
108108

109109
[xml]$document = Get-Content -LiteralPath $Path -Raw
110-
$changed = $false
110+
$changed = Repair-CodeSceneJaCoCoNodePathAttribute -Nodes $document.SelectNodes('//class[@sourcefilename]') -AttributeName 'sourcefilename'
111+
$changed = (Repair-CodeSceneJaCoCoNodePathAttribute -Nodes $document.SelectNodes('//sourcefile[@name]') -AttributeName 'name') -or $changed
111112

112-
foreach ($class in $document.SelectNodes('//class[@sourcefilename]')) {
113-
$original = $class.GetAttribute('sourcefilename')
114-
$normalized = [System.IO.Path]::GetFileName($original)
115-
if ($normalized -ne $original) {
116-
$class.SetAttribute('sourcefilename', $normalized)
117-
$changed = $true
118-
}
113+
if ($changed) {
114+
$document.Save($Path)
115+
Write-Host "Normalized JaCoCo sourcefile paths in '$Path' so package + sourcefile resolve to repo files."
119116
}
117+
}
120118

121-
foreach ($sourcefile in $document.SelectNodes('//sourcefile[@name]')) {
122-
$original = $sourcefile.GetAttribute('name')
123-
$normalized = [System.IO.Path]::GetFileName($original)
124-
if ($normalized -ne $original) {
125-
$sourcefile.SetAttribute('name', $normalized)
126-
$changed = $true
127-
}
119+
function Repair-CodeSceneJaCoCoNodePathAttribute {
120+
param(
121+
[Parameter(Mandatory)]$Nodes,
122+
[Parameter(Mandatory)][string]$AttributeName
123+
)
124+
125+
$changed = $false
126+
foreach ($node in $Nodes) {
127+
$changed = (Repair-CodeSceneJaCoCoNodeFileName -Node $node -AttributeName $AttributeName) -or $changed
128128
}
129129

130-
if ($changed) {
131-
$document.Save($Path)
132-
Write-Host "Normalized JaCoCo sourcefile paths in '$Path' so package + sourcefile resolve to repo files."
130+
return $changed
131+
}
132+
133+
function Repair-CodeSceneJaCoCoNodeFileName {
134+
param(
135+
[Parameter(Mandatory)]$Node,
136+
[Parameter(Mandatory)][string]$AttributeName
137+
)
138+
139+
$original = $Node.GetAttribute($AttributeName)
140+
$normalized = [System.IO.Path]::GetFileName($original)
141+
if ($normalized -eq $original) {
142+
return $false
133143
}
144+
145+
$Node.SetAttribute($AttributeName, $normalized)
146+
return $true
134147
}
135148

136149
function Test-JaCoCoBranchCoverageAvailable {

tests/CodeSceneAnalysis.Tests.ps1

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,34 @@ BeforeAll {
1616
Output = @($output)
1717
}
1818
}
19+
20+
function New-CodeSceneCoverageUploadRunnerContent {
21+
[CmdletBinding()]
22+
param(
23+
[Parameter(Mandatory)][pscustomobject]$Config
24+
)
25+
26+
$setLocationCommand = ''
27+
if ($Config.WorkingDirectory) {
28+
$setLocationCommand = "Set-Location '$($Config.WorkingDirectory)'"
29+
}
30+
31+
return @"
32+
function cs-coverage {
33+
param([Parameter(ValueFromRemainingArguments = `$true)][string[]]`$ArgumentList)
34+
35+
Add-Content -LiteralPath '$($Config.UploadLogPath)' -Value (`$ArgumentList -join ' ') -Encoding utf8
36+
`$global:LASTEXITCODE = 0
37+
}
38+
39+
[Environment]::SetEnvironmentVariable('CS_URL', 'https://codescene.example.test')
40+
[Environment]::SetEnvironmentVariable('CS_PROJECT_ID', '123')
41+
[Environment]::SetEnvironmentVariable('CS_ACCESS_TOKEN', 'token')
42+
$setLocationCommand
43+
44+
& '$codeSceneAnalysisScriptPath' $($Config.Invocation)
45+
"@
46+
}
1947
}
2048

2149
Describe 'Invoke-CodeSceneAnalysis' {
@@ -81,57 +109,30 @@ function Invoke-WebRequest {
81109
$outputText | Should -Match '(?s)separate from.*CS_ACCESS_TOKEN'
82110
}
83111

84-
It 'still uploads coverage when CoveragePath is provided' {
112+
It 'uploads coverage for <Name>' -ForEach @(
113+
@{ Name = 'an explicit coverage path'; Mode = 'Explicit'; UploadLogName = 'cs-coverage-upload.txt' }
114+
@{ Name = 'the discovered artifacts coverage file'; Mode = 'Discovered'; UploadLogName = 'cs-coverage-upload-discovered.txt' }
115+
) {
116+
$artifactsDir = Join-Path $TestDrive 'artifacts'
85117
$coveragePath = Join-Path $TestDrive 'coverage.xml'
86-
$uploadLogPath = Join-Path $TestDrive 'cs-coverage-upload.txt'
87-
Set-Content -LiteralPath $coveragePath -Value '<report><counter type="BRANCH" missed="1" covered="1" /></report>' -Encoding utf8
88-
89-
$runnerContent = @"
90-
function cs-coverage {
91-
param([Parameter(ValueFromRemainingArguments = `$true)][string[]]`$ArgumentList)
92-
93-
Add-Content -LiteralPath '$uploadLogPath' -Value (`$ArgumentList -join ' ') -Encoding utf8
94-
`$global:LASTEXITCODE = 0
95-
}
96-
97-
[Environment]::SetEnvironmentVariable('CS_URL', 'https://codescene.example.test')
98-
[Environment]::SetEnvironmentVariable('CS_PROJECT_ID', '123')
99-
[Environment]::SetEnvironmentVariable('CS_ACCESS_TOKEN', 'token')
100-
101-
& '$codeSceneAnalysisScriptPath' -CoveragePath '$coveragePath'
102-
"@
103-
104-
$result = Invoke-CodeSceneAnalysisTestScript -RunnerContent $runnerContent
105-
106-
$result.ExitCode | Should -Be 0 -Because ($result.Output -join [Environment]::NewLine)
107-
$uploadLog = Get-Content -LiteralPath $uploadLogPath -Raw
108-
$uploadLog | Should -BeLike "upload --format jacoco --metric line-coverage $coveragePath*"
109-
$uploadLog | Should -Match 'upload --format jacoco --metric branch-coverage'
110-
}
118+
$workingDirectory = $null
119+
$invocation = "-CoveragePath '$coveragePath'"
120+
if ($Mode -eq 'Discovered') {
121+
New-Item -ItemType Directory -Path $artifactsDir -Force | Out-Null
122+
$coveragePath = Join-Path $artifactsDir 'coverage.xml'
123+
$workingDirectory = $TestDrive
124+
$invocation = '-UploadCoverage'
125+
}
111126

112-
It 'uploads coverage from the artifacts folder when UploadCoverage is requested without CoveragePath' {
113-
$artifactsDir = Join-Path $TestDrive 'artifacts'
114-
$coveragePath = Join-Path $artifactsDir 'coverage.xml'
115-
$uploadLogPath = Join-Path $TestDrive 'cs-coverage-upload-discovered.txt'
116-
New-Item -ItemType Directory -Path $artifactsDir -Force | Out-Null
127+
$uploadLogPath = Join-Path $TestDrive $UploadLogName
117128
Set-Content -LiteralPath $coveragePath -Value '<report><counter type="BRANCH" missed="1" covered="1" /></report>' -Encoding utf8
118129

119-
$runnerContent = @"
120-
function cs-coverage {
121-
param([Parameter(ValueFromRemainingArguments = `$true)][string[]]`$ArgumentList)
122-
123-
Add-Content -LiteralPath '$uploadLogPath' -Value (`$ArgumentList -join ' ') -Encoding utf8
124-
`$global:LASTEXITCODE = 0
125-
}
126-
127-
[Environment]::SetEnvironmentVariable('CS_URL', 'https://codescene.example.test')
128-
[Environment]::SetEnvironmentVariable('CS_PROJECT_ID', '123')
129-
[Environment]::SetEnvironmentVariable('CS_ACCESS_TOKEN', 'token')
130-
Set-Location '$TestDrive'
131-
132-
& '$codeSceneAnalysisScriptPath' -UploadCoverage
133-
"@
134-
130+
$runnerContent = New-CodeSceneCoverageUploadRunnerContent -Config ([pscustomobject]@{
131+
CoveragePath = $coveragePath
132+
Invocation = $invocation
133+
UploadLogPath = $uploadLogPath
134+
WorkingDirectory = $workingDirectory
135+
})
135136
$result = Invoke-CodeSceneAnalysisTestScript -RunnerContent $runnerContent
136137

137138
$result.ExitCode | Should -Be 0 -Because ($result.Output -join [Environment]::NewLine)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function Stop-NovaOperation {
2+
param([string]$Message, [string]$ErrorId, [System.Management.Automation.ErrorCategory]$Category, $TargetObject)
3+
$exception = [System.Exception]::new($Message)
4+
$record = [System.Management.Automation.ErrorRecord]::new($exception, $ErrorId, $Category, $TargetObject)
5+
throw $record
6+
}
7+
function Get-FunctionNameFromFile {param($filePath)}

tests/private/build/AssertNovaPublicFunctionFileLayout.Tests.ps1

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@ BeforeAll {
22
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
33
. (Join-Path $projectRoot 'src/private/build/AssertNovaPublicFunctionFileLayout.ps1')
44

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)}
5+
. (Join-Path $PSScriptRoot 'AssertNovaPublicFunctionFileLayout.TestSupport.ps1')
126
}
137

148
Describe 'Get-NovaPublicFunctionFileList' {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
function Stop-NovaOperation {
2+
param([string]$Message, [string]$ErrorId, [System.Management.Automation.ErrorCategory]$Category, $TargetObject)
3+
$exception = [System.Exception]::new($Message)
4+
$record = [System.Management.Automation.ErrorRecord]::new($exception, $ErrorId, $Category, $TargetObject)
5+
throw $record
6+
}
7+
function Get-NovaBuildProjectInfo {param($ProjectInfo); return $ProjectInfo}
8+
function Get-NovaHelpLocale {param($HelpMarkdownFiles); return 'en-US'}
9+
function Measure-PlatyPSMarkdown {[CmdletBinding()] param([Parameter(ValueFromPipeline)]$Input) process {}}
10+
function Import-MarkdownCommandHelp {[CmdletBinding()] param([Parameter(ValueFromPipeline)]$Input, $Path) process {}}
11+
function Export-MamlCommandHelp {[CmdletBinding()] param([Parameter(ValueFromPipeline)]$Input, $OutputFolder) process {}}

tests/private/build/BuildHelp.Tests.ps1

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,7 @@ BeforeAll {
22
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
33
. (Join-Path $projectRoot 'src/private/build/BuildHelp.ps1')
44

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 {}}
5+
. (Join-Path $PSScriptRoot 'BuildHelp.TestSupport.ps1')
166
}
177

188
Describe 'Get-NovaHelpDocsDir' {
@@ -141,33 +131,22 @@ Describe 'Build-Help' {
141131
Assert-MockCalled Assert-NovaPlatyPSAvailable -Times 0
142132
}
143133

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' {
134+
It 'handles PlatyPS help export when context is <Name>' -ForEach @(
135+
@{ Name = 'missing'; HelpContext = $null; ExpectedExportCalls = 0 }
136+
@{ Name = 'available'; HelpContext = [pscustomobject]@{HelpMarkdownFiles=@(); CommandHelpFiles=@(); Locale='en-US'}; ExpectedExportCalls = 1 }
137+
) {
160138
$tmp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid())
161139
New-Item -ItemType Directory -Path $tmp | Out-Null
162140
Set-Content -Path (Join-Path $tmp 'X.md') -Value 'help'
163141
try {
164142
Mock Get-NovaBuildProjectInfo { [pscustomobject]@{DocsDir='/x'; ProjectName='Y'; OutputModuleDir='/o'} }
165143
Mock Get-NovaHelpMarkdownItem { @(Get-Item (Join-Path $tmp 'X.md')) }
166144
Mock Assert-NovaPlatyPSAvailable {}
167-
Mock Get-NovaHelpBuildContext { [pscustomobject]@{HelpMarkdownFiles=@(); CommandHelpFiles=@(); Locale='en-US'} }
145+
$script:buildHelpContext = $HelpContext
146+
Mock Get-NovaHelpBuildContext { $script:buildHelpContext }
168147
Mock Export-NovaGeneratedHelp {}
169148
Build-Help -ProjectInfo ([pscustomobject]@{})
170-
Assert-MockCalled Export-NovaGeneratedHelp -Times 1
149+
Assert-MockCalled Export-NovaGeneratedHelp -Times $ExpectedExportCalls
171150
} finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
172151
}
173152
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
function Stop-NovaOperation {
2+
param([string]$Message, [string]$ErrorId, [System.Management.Automation.ErrorCategory]$Category, $TargetObject)
3+
$exception = [System.Exception]::new($Message)
4+
$record = [System.Management.Automation.ErrorRecord]::new($exception, $ErrorId, $Category, $TargetObject)
5+
throw $record
6+
}
7+
function Get-NovaBuildProjectInfo {param($ProjectInfo); return $ProjectInfo}
8+
function Get-FunctionNameFromFile {param($filePath); return @('Foo')}
9+
function Get-AliasInFunctionFromFile {param($filePath); return @()}
10+
function Assert-ManifestSchema {param($Manifest, $AllowedParameter)}

tests/private/build/BuildManifest.Tests.ps1

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,7 @@ BeforeAll {
22
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
33
. (Join-Path $projectRoot 'src/private/build/BuildManifest.ps1')
44

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-FunctionNameFromFile {param($filePath); return @('Foo')}
13-
function Get-AliasInFunctionFromFile {param($filePath); return @()}
14-
function Assert-ManifestSchema {param($Manifest, $AllowedParameter)}
5+
. (Join-Path $PSScriptRoot 'BuildManifest.TestSupport.ps1')
156
}
167

178
Describe 'Build-Manifest' {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function Stop-NovaOperation {
2+
param([string]$Message, [string]$ErrorId, [System.Management.Automation.ErrorCategory]$Category, $TargetObject)
3+
$exception = [System.Exception]::new($Message)
4+
$record = [System.Management.Automation.ErrorRecord]::new($exception, $ErrorId, $Category, $TargetObject)
5+
throw $record
6+
}
7+
function Get-NovaBuildProjectInfo {param($ProjectInfo); return $ProjectInfo}
8+
function Test-ProjectSchema {param([string]$Schema); return $true}
9+
function Add-ProjectPreambleToModuleBuilder {param($Builder, $ProjectInfo)}
10+
function Get-ProjectScriptFile {param($ProjectInfo); return @()}
11+
function Add-ScriptFileContentToModuleBuilder {param($Builder, $ProjectInfo, $File)}
12+
function Invoke-NovaBuild {} # so (Get-Command Invoke-NovaBuild).Version is resolvable

0 commit comments

Comments
 (0)