From 49a6dd6f9f581e71b6e29f421a6ea553788e4c85 Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Thu, 28 May 2026 22:28:05 -0400 Subject: [PATCH 1/2] refactor(tests): bootstrap remaining standalone tests via build.ps1 Apply the delegate-to-build.ps1 bootstrap from #38 to the rest of the test scaffolds: tests/Manifest.tests.ps1 (both BeforeDiscovery and BeforeAll) and the tests/Unit/ Private and Public templates. These previously called Invoke-psake against build.psake.ps1 directly without populating the BuildHelpers env vars its properties block needs, so the standalone path was broken the same way Help.tests.ps1 was. Each guard now calls build.ps1 -Task 'Build' -Bootstrap via the call operator (&), so dependency bootstrap and BuildHelpers environment setup run through the canonical entry point and the script's terminating exit is contained to the script boundary instead of ending the Pester run. Project root is resolved with Split-Path -Parent (one level for Manifest.tests.ps1, three for the tests/Unit/ files, matching each file's existing root computation) and a single two-argument Join-Path, keeping it valid on PowerShell 5.1. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + tests/Manifest.tests.ps1 | 30 +++++++++++-------- .../Private/Invoke-{{Prefix}}Helper.tests.ps1 | 15 ++++++---- .../Public/Get-{{Prefix}}Example.tests.ps1 | 15 ++++++---- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a7748d..a5c27fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ own `CHANGELOG.md` (generated from `CHANGELOG.template.md` during init). - Renamed required PowerShell Gallery publish secret `PS_GALLERY_KEY` → `PSGALLERY_API_KEY` so the secret name matches the env var name PowerShellBuild reads (eliminating the previous mapping caveat). New modules created from the template after this change pick up the new name automatically. **Migration for existing modules:** create a new `PSGALLERY_API_KEY` repo secret with the same value, update `.github/workflows/PublishModuleToPowerShellGallery.yaml` to reference `secrets.PSGALLERY_API_KEY`, then delete the old `PS_GALLERY_KEY` secret. - Test scaffolding (`tests/Help.tests.ps1`, `tests/Manifest.tests.ps1`, `tests/Meta.tests.ps1`, `tests/MetaFixers.psm1`, and the `tests/Unit/` templates) no longer names parameters on single-argument calls (e.g. `Test-Path $path`, `Get-Module $name`, `Get-Help $command`), matching the scoped named-parameter rule — name parameters only when a call passes two or more arguments. A trailing switch counts as an argument, so `Split-Path -Path $x -Parent`, `Get-Content -Path $x -Raw`, and `Get-ChildItem -Path $x -Recurse` keep their names, as do genuinely multi-value calls (`Join-Path`, `Get-Command -Name … -CommandType …`) and `Test-Path -LiteralPath`. +- `tests/Manifest.tests.ps1` and the `tests/Unit/` templates now use the same standalone build-bootstrap approach introduced for `tests/Help.tests.ps1` (#38): their `BHBuildOutput`-missing guards delegate to `build.ps1 -Task 'Build' -Bootstrap` (invoked with `&`, not dot-sourced, so its terminating `exit` is contained) instead of calling `Invoke-psake` against `build.psake.ps1` directly. This routes dependency bootstrap and BuildHelpers environment setup through the canonical entry point, so these tests can be run standalone (e.g. from an editor or by an agent) without first running `./build.ps1`. No effect on CI or `./build.ps1` runs — the guard only fires when `BHBuildOutput` is unset. ## [2026.04.29] - 2026-04-29 diff --git a/tests/Manifest.tests.ps1 b/tests/Manifest.tests.ps1 index dd7d7bf..fbd4339 100644 --- a/tests/Manifest.tests.ps1 +++ b/tests/Manifest.tests.ps1 @@ -60,12 +60,15 @@ BeforeDiscovery { # Check if the BHBuildOutput environment variable exists to determine if this test is running in a psake # build or not. If it does not exist, it is not running in a psake build, so build the module. if ($null -eq $Env:BHBuildOutput) { - $buildFilePath = Join-Path -Path $PSScriptRoot -ChildPath '..\build.psake.ps1' - $invokePsakeParameters = @{ - TaskList = 'Build' - BuildFile = $buildFilePath - } - Invoke-psake @invokePsakeParameters + # Standalone run (e.g. Invoke-Pester on this file directly, or an agent + # running one test): the module isn't built and the BuildHelpers env vars + # aren't set. Defer to build.ps1 -- the canonical entry point -- to bootstrap + # dependencies, set the BuildHelpers environment, and stage the module. + # Invoke with & (not dot-sourcing): build.ps1 ends in an exit statement, and + # the call operator contains it to the script boundary instead of ending the + # whole Pester run. + $buildScript = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'build.ps1' + & $buildScript -Task 'Build' -Bootstrap } # PowerShellBuild outputs to Output///, override BHBuildOutput @@ -99,12 +102,15 @@ BeforeAll { # Check if the BHBuildOutput environment variable exists to determine if this test is running in a psake # build or not. If it does not exist, it is not running in a psake build, so build the module. if ($null -eq $Env:BHBuildOutput) { - $buildFilePath = Join-Path -Path $PSScriptRoot -ChildPath '..\build.psake.ps1' - $invokePsakeParameters = @{ - TaskList = 'Build' - BuildFile = $buildFilePath - } - Invoke-psake @invokePsakeParameters + # Standalone run (e.g. Invoke-Pester on this file directly, or an agent + # running one test): the module isn't built and the BuildHelpers env vars + # aren't set. Defer to build.ps1 -- the canonical entry point -- to bootstrap + # dependencies, set the BuildHelpers environment, and stage the module. + # Invoke with & (not dot-sourcing): build.ps1 ends in an exit statement, and + # the call operator contains it to the script boundary instead of ending the + # whole Pester run. + $buildScript = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'build.ps1' + & $buildScript -Task 'Build' -Bootstrap } # PowerShellBuild outputs to Output///, override BHBuildOutput diff --git a/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 b/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 index 0811a60..ee24ddd 100644 --- a/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 +++ b/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 @@ -8,12 +8,15 @@ param() BeforeDiscovery { # Build module if not running in psake build if ($null -eq $Env:BHBuildOutput) { - $buildFilePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\build.psake.ps1' - $invokePsakeParameters = @{ - TaskList = 'Build' - BuildFile = $buildFilePath - } - Invoke-psake @invokePsakeParameters + # Standalone run (e.g. Invoke-Pester on this file directly, or an agent + # running one test): the module isn't built and the BuildHelpers env vars + # aren't set. Defer to build.ps1 -- the canonical entry point -- to bootstrap + # dependencies, set the BuildHelpers environment, and stage the module. + # Invoke with & (not dot-sourcing): build.ps1 ends in an exit statement, and + # the call operator contains it to the script boundary instead of ending the + # whole Pester run. + $buildScript = Join-Path -Path (Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent) -ChildPath 'build.ps1' + & $buildScript -Task 'Build' -Bootstrap } # PowerShellBuild outputs to Output/// diff --git a/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 b/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 index 5763829..3156f74 100644 --- a/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 +++ b/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 @@ -8,12 +8,15 @@ param() BeforeDiscovery { # Build module if not running in psake build if ($null -eq $Env:BHBuildOutput) { - $buildFilePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\build.psake.ps1' - $invokePsakeParameters = @{ - TaskList = 'Build' - BuildFile = $buildFilePath - } - Invoke-psake @invokePsakeParameters + # Standalone run (e.g. Invoke-Pester on this file directly, or an agent + # running one test): the module isn't built and the BuildHelpers env vars + # aren't set. Defer to build.ps1 -- the canonical entry point -- to bootstrap + # dependencies, set the BuildHelpers environment, and stage the module. + # Invoke with & (not dot-sourcing): build.ps1 ends in an exit statement, and + # the call operator contains it to the script boundary instead of ending the + # whole Pester run. + $buildScript = Join-Path -Path (Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent) -ChildPath 'build.ps1' + & $buildScript -Task 'Build' -Bootstrap } # PowerShellBuild outputs to Output/// From 409bbda07d46906173c0b844b422a61e5c83967e Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Thu, 28 May 2026 22:37:36 -0400 Subject: [PATCH 2/2] refactor(tests): compute project root once in bootstrap Address review feedback (Copilot) on the standalone bootstrap: the project root was computed twice -- once inline for $buildScript and again a few lines later for $projectRoot -- which is exactly the kind of duplication that can drift. Hoist the single $projectRoot assignment above the BHBuildOutput guard and derive $buildScript from it via Join-Path, in Manifest.tests.ps1 (both blocks) and the tests/Unit/ Private and Public templates. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/Manifest.tests.ps1 | 14 ++++++++++---- .../Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 | 7 +++++-- tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 | 7 +++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/Manifest.tests.ps1 b/tests/Manifest.tests.ps1 index fbd4339..ddf7a18 100644 --- a/tests/Manifest.tests.ps1 +++ b/tests/Manifest.tests.ps1 @@ -57,6 +57,10 @@ param() BeforeDiscovery { + # Resolve the project root once (tests/ -> repo root); used both to locate + # build.ps1 below and to compute the staged build output path. + $projectRoot = Split-Path -Path $PSScriptRoot -Parent + # Check if the BHBuildOutput environment variable exists to determine if this test is running in a psake # build or not. If it does not exist, it is not running in a psake build, so build the module. if ($null -eq $Env:BHBuildOutput) { @@ -67,12 +71,11 @@ BeforeDiscovery { # Invoke with & (not dot-sourcing): build.ps1 ends in an exit statement, and # the call operator contains it to the script boundary instead of ending the # whole Pester run. - $buildScript = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'build.ps1' + $buildScript = Join-Path -Path $projectRoot -ChildPath 'build.ps1' & $buildScript -Task 'Build' -Bootstrap } # PowerShellBuild outputs to Output///, override BHBuildOutput - $projectRoot = Split-Path -Path $PSScriptRoot -Parent $sourceManifest = Join-Path -Path $projectRoot -ChildPath "$Env:BHProjectName/$Env:BHProjectName.psd1" $moduleVersion = (Import-PowerShellDataFile $sourceManifest).ModuleVersion $Env:BHBuildOutput = Join-Path -Path $projectRoot -ChildPath "Output/$Env:BHProjectName/$moduleVersion" @@ -99,6 +102,10 @@ BeforeDiscovery { $isTemplate = Test-Path -LiteralPath (Join-Path -Path $Env:BHProjectPath -ChildPath 'CHANGELOG.template.md') } BeforeAll { + # Resolve the project root once (tests/ -> repo root); used both to locate + # build.ps1 below and to compute the staged build output path. + $projectRoot = Split-Path -Path $PSScriptRoot -Parent + # Check if the BHBuildOutput environment variable exists to determine if this test is running in a psake # build or not. If it does not exist, it is not running in a psake build, so build the module. if ($null -eq $Env:BHBuildOutput) { @@ -109,12 +116,11 @@ BeforeAll { # Invoke with & (not dot-sourcing): build.ps1 ends in an exit statement, and # the call operator contains it to the script boundary instead of ending the # whole Pester run. - $buildScript = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'build.ps1' + $buildScript = Join-Path -Path $projectRoot -ChildPath 'build.ps1' & $buildScript -Task 'Build' -Bootstrap } # PowerShellBuild outputs to Output///, override BHBuildOutput - $projectRoot = Split-Path -Path $PSScriptRoot -Parent $sourceManifest = Join-Path -Path $projectRoot -ChildPath "$Env:BHProjectName/$Env:BHProjectName.psd1" $moduleVersion = (Import-PowerShellDataFile $sourceManifest).ModuleVersion $Env:BHBuildOutput = Join-Path -Path $projectRoot -ChildPath "Output/$Env:BHProjectName/$moduleVersion" diff --git a/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 b/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 index ee24ddd..d577e29 100644 --- a/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 +++ b/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 @@ -6,6 +6,10 @@ param() BeforeDiscovery { + # Resolve the project root once (tests/Unit// -> repo root, three levels up); + # used both to locate build.ps1 below and to compute the staged build output path. + $projectRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent + # Build module if not running in psake build if ($null -eq $Env:BHBuildOutput) { # Standalone run (e.g. Invoke-Pester on this file directly, or an agent @@ -15,12 +19,11 @@ BeforeDiscovery { # Invoke with & (not dot-sourcing): build.ps1 ends in an exit statement, and # the call operator contains it to the script boundary instead of ending the # whole Pester run. - $buildScript = Join-Path -Path (Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent) -ChildPath 'build.ps1' + $buildScript = Join-Path -Path $projectRoot -ChildPath 'build.ps1' & $buildScript -Task 'Build' -Bootstrap } # PowerShellBuild outputs to Output/// - $projectRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent $sourceManifest = Join-Path -Path $projectRoot -ChildPath "$Env:BHProjectName/$Env:BHProjectName.psd1" $moduleVersion = (Import-PowerShellDataFile $sourceManifest).ModuleVersion $Env:BHBuildOutput = Join-Path -Path $projectRoot -ChildPath "Output/$Env:BHProjectName/$moduleVersion" diff --git a/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 b/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 index 3156f74..7672096 100644 --- a/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 +++ b/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 @@ -6,6 +6,10 @@ param() BeforeDiscovery { + # Resolve the project root once (tests/Unit// -> repo root, three levels up); + # used both to locate build.ps1 below and to compute the staged build output path. + $projectRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent + # Build module if not running in psake build if ($null -eq $Env:BHBuildOutput) { # Standalone run (e.g. Invoke-Pester on this file directly, or an agent @@ -15,12 +19,11 @@ BeforeDiscovery { # Invoke with & (not dot-sourcing): build.ps1 ends in an exit statement, and # the call operator contains it to the script boundary instead of ending the # whole Pester run. - $buildScript = Join-Path -Path (Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent) -ChildPath 'build.ps1' + $buildScript = Join-Path -Path $projectRoot -ChildPath 'build.ps1' & $buildScript -Task 'Build' -Bootstrap } # PowerShellBuild outputs to Output/// - $projectRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent $sourceManifest = Join-Path -Path $projectRoot -ChildPath "$Env:BHProjectName/$Env:BHProjectName.psd1" $moduleVersion = (Import-PowerShellDataFile $sourceManifest).ModuleVersion $Env:BHBuildOutput = Join-Path -Path $projectRoot -ChildPath "Output/$Env:BHProjectName/$moduleVersion"