diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcbfaf7..e84bb609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CI bump now reuses the already activated built-module command when the current session is already running from `dist/`, so publish-then-bump prerelease automation can continue in the same session without losing private helper bindings. +- Keep standalone `nova bump` output stable by formatting version-update results in the CLI layer instead of relying on + PowerShell's default object rendering. + - `nova bump --what-if` and `% run.ps1` now surface a predictable summary for previous version, new version, label, + and commit count. ### Changed diff --git a/src/private/cli/FormatNovaCliCommandResult.ps1 b/src/private/cli/FormatNovaCliCommandResult.ps1 index 2fc42c1d..5e33e485 100644 --- a/src/private/cli/FormatNovaCliCommandResult.ps1 +++ b/src/private/cli/FormatNovaCliCommandResult.ps1 @@ -13,20 +13,61 @@ function Test-NovaCliNoUpdateResult { return ($propertyNames -contains 'UpdateAvailable') -and ($propertyNames -contains 'CurrentVersion') -and -not $Result.UpdateAvailable } -function Format-NovaCliCommandResult { +function Test-NovaCliVersionUpdateResult { [CmdletBinding()] param( [string]$Command, [object]$Result ) - if (-not (Test-NovaCliNoUpdateResult -Command $Command -Result $Result)) { - return $Result + if ($Command -ne 'bump' -or $null -eq $Result) { + return $false + } + + $propertyNames = @($Result.PSObject.Properties.Name) + foreach ($requiredPropertyName in @('PreviousVersion', 'NewVersion', 'Label', 'CommitCount', 'Applied')) { + if ($propertyNames -notcontains $requiredPropertyName) { + return $false + } + } + + return $true +} + +function Format-NovaCliVersionUpdateResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$Result + ) + + $summaryPrefix = if ($Result.Applied) { + 'Version bump completed:' } + else { + 'Version plan:' + } + + return "$summaryPrefix $( $Result.PreviousVersion ) -> $( $Result.NewVersion ) | Label: $( $Result.Label ) | Commits: $( $Result.CommitCount )" +} - return @( - "You're up to date!" - "$( $Result.ModuleName ) $( $Result.CurrentVersion ) is currently the newest version available." +function Format-NovaCliCommandResult { + [CmdletBinding()] + param( + [string]$Command, + [object]$Result ) + + if (Test-NovaCliNoUpdateResult -Command $Command -Result $Result) { + return @( + "You're up to date!" + "$( $Result.ModuleName ) $( $Result.CurrentVersion ) is currently the newest version available." + ) + } + + if (Test-NovaCliVersionUpdateResult -Command $Command -Result $Result) { + return Format-NovaCliVersionUpdateResult -Result $Result + } + + return $Result } diff --git a/src/private/cli/InvokeNovaCliCommandRoute.ps1 b/src/private/cli/InvokeNovaCliCommandRoute.ps1 index 96879322..6c34362f 100644 --- a/src/private/cli/InvokeNovaCliCommandRoute.ps1 +++ b/src/private/cli/InvokeNovaCliCommandRoute.ps1 @@ -64,6 +64,16 @@ function Invoke-NovaCliUpdateRouteCommand { Format-NovaCliCommandResult -Command $InvocationContext.Command -Result $result } +function Invoke-NovaCliBumpRouteCommand { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$InvocationContext + ) + + $result = Invoke-NovaCliParsedCommand -InvocationContext $InvocationContext -ParserCommand 'ConvertFrom-NovaBumpCliArgument' -ActionCommand 'Update-NovaModuleVersion' + return Format-NovaCliCommandResult -Command $InvocationContext.Command -Result $result +} + function Invoke-NovaCliNotificationRouteCommand { [CmdletBinding()] param( @@ -125,7 +135,7 @@ function Invoke-NovaCliCommandRoute { return Invoke-NovaCliInitCommand -Arguments $InvocationContext.Arguments -ForwardedParameters $mutatingCommonParameters -WhatIfEnabled:$InvocationContext.WhatIfEnabled } 'bump' { - return Invoke-NovaCliParsedCommand -InvocationContext $InvocationContext -ParserCommand 'ConvertFrom-NovaBumpCliArgument' -ActionCommand 'Update-NovaModuleVersion' + return Invoke-NovaCliBumpRouteCommand -InvocationContext $InvocationContext } 'update' { return Invoke-NovaCliUpdateRouteCommand -InvocationContext $InvocationContext diff --git a/src/private/cli/InvokeNovaCliInstallWorkflow.ps1 b/src/private/cli/InvokeNovaCliInstallWorkflow.ps1 index 3d402f38..2a07714a 100644 --- a/src/private/cli/InvokeNovaCliInstallWorkflow.ps1 +++ b/src/private/cli/InvokeNovaCliInstallWorkflow.ps1 @@ -10,12 +10,13 @@ function Invoke-NovaCliInstallWorkflow { Write-Warning "Installed nova to $( $WorkflowContext.TargetDirectory ), but that directory is not currently in PATH. Add it to your shell profile before using nova directly from zsh/bash." } - Write-NovaModuleReleaseNotesLink + $releaseNotesUri = Get-NovaModuleReleaseNotesUri return [pscustomobject]@{ CommandName = 'nova' InstalledPath = $installedPath DestinationDirectory = $WorkflowContext.TargetDirectory DirectoryOnPath = $directoryOnPath + ReleaseNotesUri = $releaseNotesUri } } diff --git a/src/private/release/InvokeNovaVersionUpdateWorkflow.ps1 b/src/private/release/InvokeNovaVersionUpdateWorkflow.ps1 index 27cc81fd..6ec202c1 100644 --- a/src/private/release/InvokeNovaVersionUpdateWorkflow.ps1 +++ b/src/private/release/InvokeNovaVersionUpdateWorkflow.ps1 @@ -6,15 +6,16 @@ function Invoke-NovaVersionUpdateWorkflow { [switch]$WhatIfEnabled ) + $versionWriteResult = $null if ($ShouldRun) { - Set-NovaModuleVersion -ProjectInfo $WorkflowContext.ProjectInfo -Label $WorkflowContext.Label -PreviewRelease:$WorkflowContext.PreviewRelease -Confirm:$false + $versionWriteResult = Set-NovaModuleVersion -ProjectInfo $WorkflowContext.ProjectInfo -Label $WorkflowContext.Label -PreviewRelease:$WorkflowContext.PreviewRelease -Confirm:$false } if (-not (Test-NovaVersionUpdateResultRequired -ShouldRun:$ShouldRun -WhatIfEnabled:$WhatIfEnabled)) { return } - return Get-NovaVersionUpdateResult -WorkflowContext $WorkflowContext + return Get-NovaVersionUpdateResult -WorkflowContext $WorkflowContext -Applied:($null -ne $versionWriteResult -and $versionWriteResult.Applied) } function Test-NovaVersionUpdateResultRequired { @@ -30,7 +31,8 @@ function Test-NovaVersionUpdateResultRequired { function Get-NovaVersionUpdateResult { [CmdletBinding()] param( - [Parameter(Mandatory)][pscustomobject]$WorkflowContext + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$Applied ) return [pscustomobject]@{ @@ -38,5 +40,6 @@ function Get-NovaVersionUpdateResult { NewVersion = $WorkflowContext.NewVersion Label = $WorkflowContext.Label CommitCount = $WorkflowContext.CommitCount + Applied = [bool]$Applied } } diff --git a/src/private/release/SetNovaModuleVersion.ps1 b/src/private/release/SetNovaModuleVersion.ps1 index 7d0fd3b3..9bb459aa 100644 --- a/src/private/release/SetNovaModuleVersion.ps1 +++ b/src/private/release/SetNovaModuleVersion.ps1 @@ -1,3 +1,21 @@ +function Get-NovaModuleVersionWriteResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$ProjectFile, + [Parameter(Mandatory)][string]$PreviousVersion, + [Parameter(Mandatory)][string]$NewVersion, + [switch]$Applied + ) + + return [pscustomobject]@{ + ProjectFile = $ProjectFile + Target = [System.IO.Path]::GetFileName($ProjectFile) + PreviousVersion = $PreviousVersion + NewVersion = $NewVersion + Applied = [bool]$Applied + } +} + function Set-NovaModuleVersion { [CmdletBinding(SupportsShouldProcess = $true)] param( @@ -11,13 +29,16 @@ function Set-NovaModuleVersion { $versionUpdatePlan = Get-NovaVersionUpdatePlan -ProjectInfo $ProjectInfo -Label $Label -PreviewRelease:$PreviewRelease -StableRelease:$StableRelease $jsonContent = Read-ProjectJsonData -ProjectJsonPath $versionUpdatePlan.ProjectFile + $previousVersion = [string]$jsonContent.Version $newVersion = $versionUpdatePlan.NewVersion.ToString() $target = [System.IO.Path]::GetFileName($versionUpdatePlan.ProjectFile) $action = "Set module version to $newVersion" - if ( $PSCmdlet.ShouldProcess($target, $action)) { - $jsonContent.Version = $newVersion - Write-Host "Version bumped to : $newVersion" - Write-ProjectJsonData -ProjectJsonPath $versionUpdatePlan.ProjectFile -Data $jsonContent + if (-not $PSCmdlet.ShouldProcess($target, $action)) { + return Get-NovaModuleVersionWriteResult -ProjectFile $versionUpdatePlan.ProjectFile -PreviousVersion $previousVersion -NewVersion $newVersion } + + $jsonContent.Version = $newVersion + Write-ProjectJsonData -ProjectJsonPath $versionUpdatePlan.ProjectFile -Data $jsonContent + return Get-NovaModuleVersionWriteResult -ProjectFile $versionUpdatePlan.ProjectFile -PreviousVersion $previousVersion -NewVersion $newVersion -Applied } diff --git a/src/private/shared/WriteNovaModuleReleaseNotesLink.ps1 b/src/private/shared/WriteNovaModuleReleaseNotesLink.ps1 index d73ea0fe..c12979e8 100644 --- a/src/private/shared/WriteNovaModuleReleaseNotesLink.ps1 +++ b/src/private/shared/WriteNovaModuleReleaseNotesLink.ps1 @@ -1,13 +1,36 @@ +function Get-NovaModuleReleaseNotesMessage { + [CmdletBinding(DefaultParameterSetName = 'Module')] + param( + [Parameter(ParameterSetName = 'Module')] + [object]$Module = $ExecutionContext.SessionState.Module, + [Parameter(ParameterSetName = 'Uri')] + [AllowNull()][string]$ReleaseNotesUri + ) + + if ($PSCmdlet.ParameterSetName -eq 'Module') { + $ReleaseNotesUri = Get-NovaModuleReleaseNotesUri -Module $Module + } + + if ( [string]::IsNullOrWhiteSpace($ReleaseNotesUri)) { + return $null + } + + return "Release notes: $($ReleaseNotesUri.Trim() )" +} + function Write-NovaModuleReleaseNotesLink { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'Module')] param( - [object]$Module = $ExecutionContext.SessionState.Module + [Parameter(ParameterSetName = 'Module')] + [object]$Module = $ExecutionContext.SessionState.Module, + [Parameter(ParameterSetName = 'Uri')] + [AllowNull()][string]$ReleaseNotesUri ) - $releaseNotesUri = Get-NovaModuleReleaseNotesUri -Module $Module - if ($null -eq $releaseNotesUri) { + $message = Get-NovaModuleReleaseNotesMessage @PSBoundParameters + if ($null -eq $message) { return } - Write-Host "Release notes: $releaseNotesUri" + Write-Host $message } diff --git a/src/private/update/InvokeNovaModuleSelfUpdateWorkflow.ps1 b/src/private/update/InvokeNovaModuleSelfUpdateWorkflow.ps1 index a8094ed7..3d7e834c 100644 --- a/src/private/update/InvokeNovaModuleSelfUpdateWorkflow.ps1 +++ b/src/private/update/InvokeNovaModuleSelfUpdateWorkflow.ps1 @@ -12,6 +12,22 @@ function Invoke-NovaModuleSelfUpdateOrStop { } } +function Complete-NovaModuleSelfUpdateResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Plan, + [AllowNull()][string]$ReleaseNotesUri + ) + + if ($Plan.PSObject.Properties.Name -contains 'ReleaseNotesUri') { + $Plan.ReleaseNotesUri = $ReleaseNotesUri + return $Plan + } + + $Plan | Add-Member -NotePropertyName 'ReleaseNotesUri' -NotePropertyValue $ReleaseNotesUri + return $Plan +} + function Invoke-NovaModuleSelfUpdateWorkflow { [CmdletBinding()] param( @@ -20,11 +36,11 @@ function Invoke-NovaModuleSelfUpdateWorkflow { $plan = $WorkflowContext.Plan if (-not $plan.UpdateAvailable) { - return $plan + return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $null } Invoke-NovaModuleSelfUpdateOrStop -Plan $plan $plan.Updated = $true - Write-NovaModuleReleaseNotesLink - return $plan + $releaseNotesUri = Get-NovaModuleReleaseNotesUri + return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $releaseNotesUri } diff --git a/src/public/InstallNovaCli.ps1 b/src/public/InstallNovaCli.ps1 index 1b69adc7..68849de1 100644 --- a/src/public/InstallNovaCli.ps1 +++ b/src/public/InstallNovaCli.ps1 @@ -11,5 +11,7 @@ function Install-NovaCli { return } - return Invoke-NovaCliInstallWorkflow -WorkflowContext $workflowContext + $result = Invoke-NovaCliInstallWorkflow -WorkflowContext $workflowContext + Write-NovaModuleReleaseNotesLink -ReleaseNotesUri $result.ReleaseNotesUri + return $result } diff --git a/src/public/UpdateNovaModuleTools.ps1 b/src/public/UpdateNovaModuleTools.ps1 index cefae9d1..f54082fc 100644 --- a/src/public/UpdateNovaModuleTools.ps1 +++ b/src/public/UpdateNovaModuleTools.ps1 @@ -6,17 +6,19 @@ function Update-NovaModuleTool { $workflowContext = Get-NovaModuleSelfUpdateWorkflowContext $plan = $workflowContext.Plan if (-not $plan.UpdateAvailable) { - return $plan + return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $null } if ($plan.IsPrereleaseTarget -and -not (Confirm-NovaPrereleaseModuleUpdate -Cmdlet $PSCmdlet -CurrentVersion $plan.CurrentVersion -TargetVersion $plan.TargetVersion)) { $plan.Cancelled = $true - return $plan + return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $null } if (-not $PSCmdlet.ShouldProcess($plan.ModuleName, $workflowContext.Action)) { - return $plan + return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $null } - return Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext $workflowContext + $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext $workflowContext + Write-NovaModuleReleaseNotesLink -ReleaseNotesUri $result.ReleaseNotesUri + return $result } diff --git a/src/public/UpdateNovaModuleVersion.ps1 b/src/public/UpdateNovaModuleVersion.ps1 index 53d6760b..2522c871 100644 --- a/src/public/UpdateNovaModuleVersion.ps1 +++ b/src/public/UpdateNovaModuleVersion.ps1 @@ -19,5 +19,14 @@ function Update-NovaModuleVersion { $shouldRun = $PSCmdlet.ShouldProcess($workflowContext.Target, $workflowContext.Action) - return Invoke-NovaVersionUpdateWorkflow -WorkflowContext $workflowContext -ShouldRun:$shouldRun -WhatIfEnabled:$WhatIfPreference + $result = Invoke-NovaVersionUpdateWorkflow -WorkflowContext $workflowContext -ShouldRun:$shouldRun -WhatIfEnabled:$WhatIfPreference + if ($null -eq $result) { + return + } + + if ($result.Applied) { + Write-Host "Version bumped to : $( $result.NewVersion )" + } + + return $result } diff --git a/tests/CoverageGaps.Cli.Tests.ps1 b/tests/CoverageGaps.Cli.Tests.ps1 index 8be6856c..84e098c7 100644 --- a/tests/CoverageGaps.Cli.Tests.ps1 +++ b/tests/CoverageGaps.Cli.Tests.ps1 @@ -125,6 +125,69 @@ Describe 'Coverage gaps for CLI and installed-version internals' { } } + It 'Format-NovaCliCommandResult renders structured bump results as a stable CLI summary' { + InModuleScope $script:moduleName { + $versionUpdateResult = [pscustomobject]@{ + PreviousVersion = '1.0.0' + NewVersion = '1.1.0-preview' + Label = 'Minor' + CommitCount = 2 + Applied = $false + } + $result = Format-NovaCliCommandResult -Command 'bump' -Result $versionUpdateResult + + $result | Should -Be 'Version plan: 1.0.0 -> 1.1.0-preview | Label: Minor | Commits: 2' + } + } + + It 'Format-NovaCliCommandResult renders applied bump results as a completed CLI summary' { + InModuleScope $script:moduleName { + $versionUpdateResult = [pscustomobject]@{ + PreviousVersion = '1.0.0' + NewVersion = '1.1.0' + Label = 'Minor' + CommitCount = 2 + Applied = $true + } + + $result = Format-NovaCliCommandResult -Command 'bump' -Result $versionUpdateResult + + $result | Should -Be 'Version bump completed: 1.0.0 -> 1.1.0 | Label: Minor | Commits: 2' + } + } + + It 'Invoke-NovaCliCommandRoute formats bump results through the shared CLI formatter' { + InModuleScope $script:moduleName { + $invocationContext = [pscustomobject]@{ + Command = 'bump' + Arguments = @('--preview') + CommonParameters = @{} + MutatingCommonParameters = @{WhatIf = $true} + IsHelpRequest = $false + HelpRequest = $null + ModuleName = 'NovaModuleTools' + WhatIfEnabled = $true + CliConfirmEnabled = $false + } + Mock ConvertFrom-NovaBumpCliArgument {@{Preview = $true}} + Mock Update-NovaModuleVersion { + [pscustomobject]@{ + PreviousVersion = '1.0.0' + NewVersion = '1.1.0-preview' + Label = 'Minor' + CommitCount = 2 + Applied = $false + } + } + + $result = Invoke-NovaCliCommandRoute -InvocationContext $invocationContext + + $result | Should -Be 'Version plan: 1.0.0 -> 1.1.0-preview | Label: Minor | Commits: 2' + Assert-MockCalled ConvertFrom-NovaBumpCliArgument -Times 1 -ParameterFilter {$Arguments -eq @('--preview')} + Assert-MockCalled Update-NovaModuleVersion -Times 1 -ParameterFilter {$Preview -and $WhatIf} + } + } + It 'Invoke-NovaCliCommandRoute handles the direct root --help command route when help was not pre-normalized' { InModuleScope $script:moduleName { $invocationContext = [pscustomobject]@{ diff --git a/tests/CoverageGaps.ReleaseInternals.Tests.ps1 b/tests/CoverageGaps.ReleaseInternals.Tests.ps1 index ac29ab4b..6b0379ca 100644 --- a/tests/CoverageGaps.ReleaseInternals.Tests.ps1 +++ b/tests/CoverageGaps.ReleaseInternals.Tests.ps1 @@ -198,13 +198,15 @@ Describe 'Coverage gaps for release and git internals' { } '@ Mock Get-NovaProjectInfo {[pscustomobject]@{ProjectJSON = $projectJsonPath}} - Mock Write-Host {} $warningMessages = $null - Set-NovaModuleVersion -Label Minor -PreviewRelease -Confirm:$false -WarningVariable warningMessages + $result = Set-NovaModuleVersion -Label Minor -PreviewRelease -Confirm:$false -WarningVariable warningMessages $updatedProject = Get-Content -LiteralPath $projectJsonPath -Raw | ConvertFrom-Json + $result.PreviousVersion | Should -Be '1.2.3' + $result.NewVersion | Should -Be '1.3.0-preview' + $result.Applied | Should -BeTrue $updatedProject.Version | Should -Be '1.3.0-preview' $updatedProject.Package.Auth.HeaderName | Should -Be 'Authorization' $updatedProject.Package.Repositories.Count | Should -Be 1 @@ -236,10 +238,13 @@ Describe 'Coverage gaps for release and git internals' { } } Mock Write-ProjectJsonData {} - Mock Write-Host {} - Set-NovaModuleVersion -Label Minor -PreviewRelease -Confirm:$false + $result = Set-NovaModuleVersion -Label Minor -PreviewRelease -Confirm:$false + $result.ProjectFile | Should -Be '/tmp/project.json' + $result.PreviousVersion | Should -Be '1.2.3' + $result.NewVersion | Should -Be '1.3.0-preview' + $result.Applied | Should -BeTrue Assert-MockCalled Read-ProjectJsonData -Times 1 -ParameterFilter {$ProjectJsonPath -eq '/tmp/project.json'} Assert-MockCalled Write-ProjectJsonData -Times 1 -ParameterFilter { $ProjectJsonPath -eq '/tmp/project.json' -and @@ -249,6 +254,32 @@ Describe 'Coverage gaps for release and git internals' { } } + It 'Set-NovaModuleVersion returns a non-applied write result when WhatIf declines project.json persistence' { + InModuleScope $script:moduleName { + Mock Get-NovaVersionUpdatePlan { + [pscustomobject]@{ + ProjectFile = '/tmp/project.json' + NewVersion = [semver]'1.3.0-preview' + } + } + Mock Read-ProjectJsonData { + [ordered]@{ + Version = '1.2.3' + } + } + Mock Write-ProjectJsonData {throw 'should not persist during WhatIf'} + + $result = Set-NovaModuleVersion -Label Minor -PreviewRelease -WhatIf + + $result.ProjectFile | Should -Be '/tmp/project.json' + $result.PreviousVersion | Should -Be '1.2.3' + $result.NewVersion | Should -Be '1.3.0-preview' + $result.Applied | Should -BeFalse + Assert-MockCalled Read-ProjectJsonData -Times 1 -ParameterFilter {$ProjectJsonPath -eq '/tmp/project.json'} + Assert-MockCalled Write-ProjectJsonData -Times 0 + } + } + It 'Publish-NovaBuiltModuleToRepository uses the PSGallery fallback api key without forcing verbose output' { InModuleScope $script:moduleName { $originalApiKey = $env:PSGALLERY_API diff --git a/tests/NovaCommandModel.BumpAndCli.Tests.ps1 b/tests/NovaCommandModel.BumpAndCli.Tests.ps1 index f0cac417..3b40bee0 100644 --- a/tests/NovaCommandModel.BumpAndCli.Tests.ps1 +++ b/tests/NovaCommandModel.BumpAndCli.Tests.ps1 @@ -207,13 +207,21 @@ Describe 'Nova command model - bump and CLI confirmation behavior' { NewVersion = '1.1.0-preview' CommitCount = 2 } - Mock Set-NovaModuleVersion {} + Mock Set-NovaModuleVersion { + [pscustomobject]@{ + PreviousVersion = '1.0.0' + NewVersion = '1.1.0-preview' + Applied = $true + } + } $whatIfResult = Invoke-NovaVersionUpdateWorkflow -WorkflowContext $workflowContext -WhatIfEnabled $runResult = Invoke-NovaVersionUpdateWorkflow -WorkflowContext $workflowContext -ShouldRun $whatIfResult.NewVersion | Should -Be '1.1.0-preview' + $whatIfResult.Applied | Should -BeFalse $runResult.NewVersion | Should -Be '1.1.0-preview' + $runResult.Applied | Should -BeTrue Assert-MockCalled Set-NovaModuleVersion -Times 1 -ParameterFilter { $ProjectInfo.ProjectName -eq 'NovaModuleTools' -and $Label -eq 'Minor' -and @@ -232,8 +240,9 @@ Describe 'Nova command model - bump and CLI confirmation behavior' { } } Mock Invoke-NovaVersionUpdateWorkflow { - [pscustomobject]@{NewVersion = '1.1.0'} + [pscustomobject]@{NewVersion = '1.1.0'; Applied = $true} } + Mock Write-Host {} $result = Update-NovaModuleVersion -Path (Get-Location).Path -Confirm:$false @@ -244,6 +253,7 @@ Describe 'Nova command model - bump and CLI confirmation behavior' { $ShouldRun -and -not $WhatIfEnabled } + Assert-MockCalled Write-Host -Times 1 -ParameterFilter {$Object -eq 'Version bumped to : 1.1.0'} } } @@ -285,8 +295,9 @@ Describe 'Nova command model - bump and CLI confirmation behavior' { } } Mock Invoke-NovaVersionUpdateWorkflow { - [pscustomobject]@{NewVersion = '1.1.0'} + [pscustomobject]@{NewVersion = '1.1.0'; Applied = $true} } + Mock Write-Host {} $result = Update-NovaModuleVersion -Path '/tmp/current-project' -ContinuousIntegration -Confirm:$false @@ -315,12 +326,14 @@ Describe 'Nova command model - bump and CLI confirmation behavior' { } } Mock Invoke-NovaVersionUpdateWorkflow { - [pscustomobject]@{NewVersion = '1.1.0'} + [pscustomobject]@{NewVersion = '1.1.0'; Applied = $false} } + Mock Write-Host {throw 'WhatIf should not emit host output'} $result = Update-NovaModuleVersion -Path '/tmp/current-project' -ContinuousIntegration -WhatIf $result.NewVersion | Should -Be '1.1.0' + $result.Applied | Should -BeFalse Assert-MockCalled Get-NovaVersionUpdateCiActivatedCommand -Times 0 Assert-MockCalled Get-NovaVersionUpdateWorkflowContext -Times 1 -ParameterFilter {$ContinuousIntegrationRequested} } @@ -523,8 +536,9 @@ Describe 'Nova command model - bump and CLI confirmation behavior' { } } Mock Invoke-NovaVersionUpdateWorkflow { - [pscustomobject]@{NewVersion = '1.1.0'} + [pscustomobject]@{NewVersion = '1.1.0'; Applied = $true} } + Mock Write-Host {} $result = Update-NovaModuleVersion -Path (Get-Location).Path -Confirm:$false diff --git a/tests/NovaCommandModel.StandaloneCli.Tests.ps1 b/tests/NovaCommandModel.StandaloneCli.Tests.ps1 index bf79db29..a6b80109 100644 --- a/tests/NovaCommandModel.StandaloneCli.Tests.ps1 +++ b/tests/NovaCommandModel.StandaloneCli.Tests.ps1 @@ -301,7 +301,7 @@ Describe '$projectName tests' { $previewBumpResult.ExitCode | Should -Be 0 $previewBumpResult.Text | Should -Match 'What if:' - $previewBumpResult.Text | Should -Match '0\.0\.1-rc1\s+0\.0\.1-rc2\s+Major\s+1' + $previewBumpResult.Text | Should -Match 'Version plan: 0\.0\.1-rc1 -> 0\.0\.1-rc2 \| Label: Major \| Commits: 1' $previewBumpResult.Text | Should -Not -Match 'Unknown argument:' $previewBumpResult.Text | Should -Not -Match 'Version bumped to :' $versionAfterBump | Should -Be '0.0.1-rc1' @@ -835,7 +835,7 @@ Describe '$projectName tests' { [pscustomobject]@{WhatIfSeen = $WhatIfPreference} } - $result = Invoke-NovaCli publish --repository PSGallery --api-key key123 -WhatIf + $result = Invoke-NovaCli -Command publish -Arguments @('--repository', 'PSGallery', '--api-key', 'key123') -WhatIf $result.WhatIfSeen | Should -BeTrue } diff --git a/tests/NovaCommandModel.TestSupport/Assertions.ps1 b/tests/NovaCommandModel.TestSupport/Assertions.ps1 index 23ec4eae..529f0fe5 100644 --- a/tests/NovaCommandModel.TestSupport/Assertions.ps1 +++ b/tests/NovaCommandModel.TestSupport/Assertions.ps1 @@ -124,8 +124,9 @@ function Invoke-UpdateNovaModuleVersionDefaultPathAssertion { } } Mock Invoke-NovaVersionUpdateWorkflow { - [pscustomobject]@{NewVersion = '1.1.0'} + [pscustomobject]@{NewVersion = '1.1.0'; Applied = $true} } + Mock Write-Host {} $result = Update-NovaModuleVersion -Confirm:$false diff --git a/tests/NovaCommandModel.TestSupport/CliProjectSupport.ps1 b/tests/NovaCommandModel.TestSupport/CliProjectSupport.ps1 index bdac1a1c..4baa9a69 100644 --- a/tests/NovaCommandModel.TestSupport/CliProjectSupport.ps1 +++ b/tests/NovaCommandModel.TestSupport/CliProjectSupport.ps1 @@ -163,9 +163,9 @@ function Assert-TestNovaCliWhatIfResultMap { $ResultMap[$resultName].Text | Should -Not -Match 'Unknown argument:' } - $ResultMap.Bump.Text | Should -Match '0\.0\.1\s+0\.1\.0\s+Minor\s+1' - $ResultMap.BumpCi.Text | Should -Match '0\.0\.1\s+0\.1\.0\s+Minor\s+1' - $ResultMap.PreviewBump.Text | Should -Match '0\.0\.1\s+0\.1\.0-preview\s+Minor\s+1' + $ResultMap.Bump.Text | Should -Match 'Version plan: 0\.0\.1 -> 0\.1\.0 \| Label: Minor \| Commits: 1' + $ResultMap.BumpCi.Text | Should -Match 'Version plan: 0\.0\.1 -> 0\.1\.0 \| Label: Minor \| Commits: 1' + $ResultMap.PreviewBump.Text | Should -Match 'Version plan: 0\.0\.1 -> 0\.1\.0-preview \| Label: Minor \| Commits: 1' $ResultMap.Bump.Text | Should -Not -Match 'Version bumped to :' $ResultMap.PreviewBump.Text | Should -Not -Match 'Version bumped to :' ((Get-Content -LiteralPath $ProjectJsonPath -Raw | ConvertFrom-Json).Version) | Should -Be '0.0.1' diff --git a/tests/RemainingCommandCoverage.Tests.ps1 b/tests/RemainingCommandCoverage.Tests.ps1 index 0c6edb69..273707d7 100644 --- a/tests/RemainingCommandCoverage.Tests.ps1 +++ b/tests/RemainingCommandCoverage.Tests.ps1 @@ -298,7 +298,7 @@ Describe 'Coverage for remaining command and filesystem branches' { } Mock Copy-NovaCliLauncher {'/tmp/bin/nova'} Mock Test-NovaCliDirectoryOnPath {$true} - Mock Write-NovaModuleReleaseNotesLink {} + Mock Get-NovaModuleReleaseNotesUri {'https://www.novamoduletools.com/release-notes.html'} $result = Invoke-NovaCliInstallWorkflow -WorkflowContext $workflowContext @@ -306,12 +306,13 @@ Describe 'Coverage for remaining command and filesystem branches' { $result.InstalledPath | Should -Be '/tmp/bin/nova' $result.DestinationDirectory | Should -Be '/tmp/bin' $result.DirectoryOnPath | Should -BeTrue + $result.ReleaseNotesUri | Should -Be 'https://www.novamoduletools.com/release-notes.html' Assert-MockCalled Copy-NovaCliLauncher -Times 1 -ParameterFilter { $SourcePath -eq '/tmp/source/nova' -and $TargetPath -eq '/tmp/bin/nova' -and $Force } - Assert-MockCalled Write-NovaModuleReleaseNotesLink -Times 1 + Assert-MockCalled Get-NovaModuleReleaseNotesUri -Times 1 } } @@ -332,8 +333,10 @@ Describe 'Coverage for remaining command and filesystem branches' { InstalledPath = '/tmp/bin/nova' DestinationDirectory = '/tmp/bin' DirectoryOnPath = $true + ReleaseNotesUri = 'https://www.novamoduletools.com/release-notes.html' } } + Mock Write-NovaModuleReleaseNotesLink {} $result = Install-NovaCli -DestinationDirectory '/tmp/bin' -Force -Confirm:$false @@ -345,6 +348,9 @@ Describe 'Coverage for remaining command and filesystem branches' { $WorkflowContext.TargetPath -eq '/tmp/bin/nova' -and $WorkflowContext.Action -eq 'Install nova CLI launcher' } + Assert-MockCalled Write-NovaModuleReleaseNotesLink -Times 1 -ParameterFilter { + $ReleaseNotesUri -eq 'https://www.novamoduletools.com/release-notes.html' + } } } diff --git a/tests/UpdateNotification.Tests.ps1 b/tests/UpdateNotification.Tests.ps1 index c2c3269b..a22819b2 100644 --- a/tests/UpdateNotification.Tests.ps1 +++ b/tests/UpdateNotification.Tests.ps1 @@ -244,7 +244,8 @@ Describe 'Update notification behavior' { UpdateAvailable = $true ExpectedUpdated = $true ExpectedUpdateCalls = 1 - ExpectedReleaseNotesCalls = 1 + ExpectedReleaseNotesUri = 'https://www.novamoduletools.com/release-notes.html' + ExpectedReleaseNotesLookups = 1 } @{ Name = 'no update available' @@ -252,12 +253,14 @@ Describe 'Update notification behavior' { UpdateAvailable = $false ExpectedUpdated = $false ExpectedUpdateCalls = 0 - ExpectedReleaseNotesCalls = 0 + ExpectedReleaseNotesUri = $null + ExpectedReleaseNotesLookups = 0 } )) { InModuleScope $script:moduleName -Parameters @{TestCase = $testCase} { param($TestCase) + $script:releaseNotesLookupCount = 0 $workflowContext = [pscustomobject]@{ Plan = [pscustomobject]@{ ModuleName = 'NovaModuleTools' @@ -274,21 +277,49 @@ Describe 'Update notification behavior' { } if ($TestCase.ExpectedUpdateCalls -eq 0) { Mock Invoke-NovaModuleSelfUpdate {throw 'should not update'} - Mock Write-NovaModuleReleaseNotesLink {throw 'should not write'} + Mock Get-NovaModuleReleaseNotesUri { + $script:releaseNotesLookupCount++ + throw 'should not look up release notes' + } } else { Mock Invoke-NovaModuleSelfUpdate {} - Mock Write-NovaModuleReleaseNotesLink {} + Mock Get-NovaModuleReleaseNotesUri { + $script:releaseNotesLookupCount++ + 'https://www.novamoduletools.com/release-notes.html' + } } $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext $workflowContext $result.Updated | Should -Be $TestCase.ExpectedUpdated -Because $TestCase.Name + if ($null -eq $TestCase.ExpectedReleaseNotesUri) { + $result.ReleaseNotesUri | Should -BeNullOrEmpty -Because $TestCase.Name + } + else { + $result.ReleaseNotesUri | Should -Be $TestCase.ExpectedReleaseNotesUri -Because $TestCase.Name + } if ($TestCase.ExpectedUpdateCalls -gt 0) { Assert-MockCalled Invoke-NovaModuleSelfUpdate -Times $TestCase.ExpectedUpdateCalls - Assert-MockCalled Write-NovaModuleReleaseNotesLink -Times $TestCase.ExpectedReleaseNotesCalls } + + $script:releaseNotesLookupCount | Should -Be $TestCase.ExpectedReleaseNotesLookups + } + } + } + + It 'Complete-NovaModuleSelfUpdateResult overwrites an existing release-notes property in place' { + InModuleScope $script:moduleName { + $plan = [pscustomobject]@{ + ModuleName = 'NovaModuleTools' + UpdateAvailable = $true + ReleaseNotesUri = 'https://old.example/release-notes' } + + $result = Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri 'https://www.novamoduletools.com/release-notes.html' + + $result.ReleaseNotesUri | Should -Be 'https://www.novamoduletools.com/release-notes.html' + @($result.PSObject.Properties.Name | Where-Object {$_ -eq 'ReleaseNotesUri'}).Count | Should -Be 1 } } @@ -311,18 +342,56 @@ Describe 'Update notification behavior' { } } Mock Invoke-NovaModuleSelfUpdateWorkflow { + $WorkflowContext.Plan | Add-Member -NotePropertyName 'ReleaseNotesUri' -NotePropertyValue 'https://www.novamoduletools.com/release-notes.html' $WorkflowContext.Plan.Updated = $true return $WorkflowContext.Plan } + Mock Write-NovaModuleReleaseNotesLink {} $result = Update-NovaModuleTool -Confirm:$false $result.Updated | Should -BeTrue + $result.ReleaseNotesUri | Should -Be 'https://www.novamoduletools.com/release-notes.html' Assert-MockCalled Get-NovaModuleSelfUpdateWorkflowContext -Times 1 Assert-MockCalled Invoke-NovaModuleSelfUpdateWorkflow -Times 1 -ParameterFilter { $WorkflowContext.Action -eq 'Update NovaModuleTools to version 1.1.0' -and $WorkflowContext.Plan.ModuleName -eq 'NovaModuleTools' } + Assert-MockCalled Write-NovaModuleReleaseNotesLink -Times 1 -ParameterFilter { + $ReleaseNotesUri -eq 'https://www.novamoduletools.com/release-notes.html' + } + } + } + + It 'Update-NovaModuleTool returns the unchanged plan and skips update execution when WhatIf declines the update action' { + InModuleScope $script:moduleName { + Mock Get-NovaModuleSelfUpdateWorkflowContext { + [pscustomobject]@{ + Plan = [pscustomobject]@{ + ModuleName = 'NovaModuleTools' + CurrentVersion = '1.0.0' + TargetVersion = '1.1.0' + PrereleaseNotificationsEnabled = $true + UpdateAvailable = $true + Updated = $false + Cancelled = $false + IsPrereleaseTarget = $false + UsedAllowPrerelease = $false + } + Action = 'Update NovaModuleTools to version 1.1.0' + } + } + Mock Invoke-NovaModuleSelfUpdateWorkflow {throw 'should not update during WhatIf'} + Mock Write-NovaModuleReleaseNotesLink {throw 'should not write release notes during WhatIf'} + + $result = Update-NovaModuleTool -WhatIf + + $result.UpdateAvailable | Should -BeTrue + $result.Updated | Should -BeFalse + $result.ReleaseNotesUri | Should -BeNullOrEmpty + Assert-MockCalled Get-NovaModuleSelfUpdateWorkflowContext -Times 1 + Assert-MockCalled Invoke-NovaModuleSelfUpdateWorkflow -Times 0 + Assert-MockCalled Write-NovaModuleReleaseNotesLink -Times 0 } } @@ -589,6 +658,31 @@ throw 'offline' } } + It 'Write-NovaModuleReleaseNotesLink formats a provided release-notes URI for user-facing output' { + InModuleScope $script:moduleName { + $script:hostMessages = @() + + Mock Write-Host {$script:hostMessages += $Object} + + $message = Get-NovaModuleReleaseNotesMessage -ReleaseNotesUri 'https://www.novamoduletools.com/release-notes.html' + Write-NovaModuleReleaseNotesLink -ReleaseNotesUri 'https://www.novamoduletools.com/release-notes.html' + + $message | Should -Be 'Release notes: https://www.novamoduletools.com/release-notes.html' + $script:hostMessages | Should -Be @('Release notes: https://www.novamoduletools.com/release-notes.html') + } + } + + It 'Get-NovaModuleReleaseNotesMessage resolves the URI from the module parameter set' { + InModuleScope $script:moduleName { + Mock Get-NovaModuleReleaseNotesUri {'https://www.novamoduletools.com/release-notes.html'} + + $message = Get-NovaModuleReleaseNotesMessage -Module ([pscustomobject]@{Name = 'NovaModuleTools'}) + + $message | Should -Be 'Release notes: https://www.novamoduletools.com/release-notes.html' + Assert-MockCalled Get-NovaModuleReleaseNotesUri -Times 1 + } + } + It 'Update-NovaModuleTool applies a stable update without prerelease confirmation' { $result = Invoke-TestNovaSelfUpdate -Options ([pscustomobject]@{ PrereleaseNotificationsEnabled = $true @@ -622,6 +716,7 @@ throw 'offline' }) $result.Result.Updated | Should -BeTrue + $result.Result.ReleaseNotesUri | Should -Be 'https://www.novamoduletools.com/release-notes.html' $result.HostMessages | Should -Contain 'Release notes: https://www.novamoduletools.com/release-notes.html' } @@ -638,6 +733,7 @@ throw 'offline' }) $result.Result.Updated | Should -BeTrue + $result.Result.ReleaseNotesUri | Should -Be 'https://www.novamoduletools.com/release-notes.html' $result.HostMessages | Should -Contain 'Release notes: https://www.novamoduletools.com/release-notes.html' } @@ -654,6 +750,7 @@ throw 'offline' }) $result.Result.Updated | Should -BeTrue + $result.Result.ReleaseNotesUri | Should -BeNullOrEmpty $result.HostMessages | Should -HaveCount 0 }