From 4d31df15cb900de7e960f26f81442eef63ab1ac2 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 18:31:38 +0200 Subject: [PATCH 1/6] Add a Pester GUID-pin test to the version-constraint suite New Action-Test job runs a test whose #Requires pins Pester by module identity (GUID) plus version, and asserts the loaded module Guid and Version. Wired into CatchJob needs, aggregated env vars, and Test-ActionResults.ps1. Related to #68. --- .github/workflows/Action-Test.yml | 36 +++++++++++++++++++ .../PesterGuidPin/PesterVersion.Tests.ps1 | 13 +++++++ tests/Test-ActionResults.ps1 | 7 ++++ 3 files changed, 56 insertions(+) create mode 100644 tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1 diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index 72da746b..ce57a03f 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -364,6 +364,37 @@ jobs: CONCLUSION: ${{ steps.action-test.conclusion }} run: tests/Show-Status.ps1 + ActionTestPesterGuidPin: + name: Action-Test - [Pester GUID Pin] + runs-on: ubuntu-latest + outputs: + Outcome: ${{ steps.action-test.outcome }} + Conclusion: ${{ steps.action-test.conclusion }} + Executed: ${{ steps.action-test.outputs.Executed }} + Result: ${{ steps.action-test.outputs.Result }} + + steps: + # Need to check out as part of the test, as it's a local action + - name: Checkout repo + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Action-Test [Pester GUID Pin] + uses: ./ + id: action-test + with: + WorkingDirectory: tests/4-PesterVersionConstraints/PesterGuidPin + Version: '6.0.0' + TestResult_TestSuiteName: PesterGuidPin + + - name: Status + shell: pwsh + env: + OUTCOME: ${{ steps.action-test.outcome }} + CONCLUSION: ${{ steps.action-test.conclusion }} + run: tests/Show-Status.ps1 + CatchJob: name: Catch Job - Aggregate Status needs: @@ -378,6 +409,7 @@ jobs: - ActionTest3Advanced - ActionTestPester5RangeConstraint - ActionTestPester6ExactConstraint + - ActionTestPesterGuidPin if: always() runs-on: ubuntu-latest steps: @@ -433,5 +465,9 @@ jobs: ACTIONTESTPESTER6EXACTCONSTRAINT_CONCLUSION: ${{ needs.ActionTestPester6ExactConstraint.outputs.Conclusion }} ACTIONTESTPESTER6EXACTCONSTRAINT_EXECUTED: ${{ needs.ActionTestPester6ExactConstraint.outputs.Executed }} ACTIONTESTPESTER6EXACTCONSTRAINT_RESULT: ${{ needs.ActionTestPester6ExactConstraint.outputs.Result }} + ACTIONTESTPESTERGUIDPIN_OUTCOME: ${{ needs.ActionTestPesterGuidPin.outputs.Outcome }} + ACTIONTESTPESTERGUIDPIN_CONCLUSION: ${{ needs.ActionTestPesterGuidPin.outputs.Conclusion }} + ACTIONTESTPESTERGUIDPIN_EXECUTED: ${{ needs.ActionTestPesterGuidPin.outputs.Executed }} + ACTIONTESTPESTERGUIDPIN_RESULT: ${{ needs.ActionTestPesterGuidPin.outputs.Result }} with: Script: tests/Test-ActionResults.ps1 diff --git a/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1 b/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1 new file mode 100644 index 00000000..65bc201f --- /dev/null +++ b/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1 @@ -0,0 +1,13 @@ +#Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '6.0.0'; GUID = 'a699dea5-2c73-4616-a270-1f7abb777e71' } + +Describe 'Pester GUID pin' { + It 'loads the Pester module identified by its GUID' { + # The #Requires above pins Pester by module identity (GUID) in addition to version. PowerShell refuses to + # run this file unless the loaded Pester matches both, so reaching this assertion already proves the pin held. + $module = Get-Module -Name Pester | Sort-Object Version -Descending | Select-Object -First 1 + + $module | Should -Not -Be $null + $module.Guid | Should -Be ([guid]'a699dea5-2c73-4616-a270-1f7abb777e71') + $module.Version | Should -Be ([version]'6.0.0') + } +} diff --git a/tests/Test-ActionResults.ps1 b/tests/Test-ActionResults.ps1 index 5167d3ce..99616bec 100644 --- a/tests/Test-ActionResults.ps1 +++ b/tests/Test-ActionResults.ps1 @@ -108,6 +108,13 @@ $jobs = @( Executed = @{ Actual = $env:ACTIONTESTPESTER6EXACTCONSTRAINT_EXECUTED; Expected = 'True' } Result = @{ Actual = $env:ACTIONTESTPESTER6EXACTCONSTRAINT_RESULT; Expected = 'Passed' } } + @{ + Name = 'Action-Test - [Pester GUID Pin]' + Outcome = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_OUTCOME; Expected = 'success' } + Conclusion = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_CONCLUSION; Expected = 'success' } + Executed = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_EXECUTED; Expected = 'True' } + Result = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_RESULT; Expected = 'Passed' } + } ) # Add Pass property to each check and convert to PSCustomObject for table output From 745189d651c04d1b7e3955e9a233d341929e88a2 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 19:42:14 +0200 Subject: [PATCH 2/6] Add optional Guid input to pin Pester module identity New Guid action input validates the loaded Pester module GUID after install (via Import-Module identity check), failing fast at init when a different module named Pester is present. Wired through action.yml, init.ps1 and exec.ps1. The existing PesterGuidPin job now also passes Guid (validating at both install and #Requires layers), and a new PesterGuidMismatch job asserts a wrong GUID fails the action. Documented in README. Related to #68. --- .github/workflows/Action-Test.yml | 33 +++++++++++++++++++++++++++++++ README.md | 1 + action.yml | 8 ++++++++ src/Invoke-Pester.Helpers.psm1 | 15 ++++++++++++++ src/exec.ps1 | 5 +++-- src/init.ps1 | 3 ++- 6 files changed, 62 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index ce57a03f..955c8615 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -386,6 +386,7 @@ jobs: with: WorkingDirectory: tests/4-PesterVersionConstraints/PesterGuidPin Version: '6.0.0' + Guid: 'a699dea5-2c73-4616-a270-1f7abb777e71' TestResult_TestSuiteName: PesterGuidPin - name: Status @@ -395,6 +396,38 @@ jobs: CONCLUSION: ${{ steps.action-test.conclusion }} run: tests/Show-Status.ps1 + ActionTestPesterGuidMismatch: + name: Action-Test - [Pester GUID Mismatch] + runs-on: ubuntu-latest + + steps: + # Need to check out as part of the test, as it's a local action + - name: Checkout repo + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + # A wrong GUID must make the action fail fast during init, before any tests run. + - name: Action-Test [Pester GUID Mismatch] + uses: ./ + id: action-test + continue-on-error: true + with: + WorkingDirectory: tests/2-Standard + Version: '6.0.0' + Guid: '00000000-0000-0000-0000-000000000000' + + - name: Assert the action failed on the GUID mismatch + shell: pwsh + env: + OUTCOME: ${{ steps.action-test.outcome }} + run: | + if ($env:OUTCOME -ne 'failure') { + Write-Error "Expected the action to fail on a GUID mismatch, but the outcome was '$env:OUTCOME'." + exit 1 + } + Write-Output 'Action failed as expected on the GUID mismatch.' + CatchJob: name: Catch Job - Aggregate Status needs: diff --git a/README.md b/README.md index 5df868bb..1c2123da 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ jobs: | `Path` | Path to where tests are located or a configuration file. | *(none)* | | `Version` | Version of Pester to install (NuGet range, e.g. `[6.0.0,7.0.0)` for any 6.x). Empty installs the latest version. | *(none)* | | `Prerelease` | Allow installing prerelease versions of Pester. | `false` | +| `Guid` | Optional module identity (GUID) the installed Pester must match; fails fast if a different module named Pester is loaded. Mirrors the GUID key of a `#Requires -Modules` pin. | *(none)* | | `ReportAsJson` | Output generated reports in JSON format in addition to the configured format through Pester. | `true` | | `Prescript` | Script to be executed before the test run. This script is executed in the same context as the test run. | *(none)* | | `Notice_Mode` | Controls when to show notices for test completion.
| `Failed` | diff --git a/action.yml b/action.yml index f0e7f499..99301eb6 100644 --- a/action.yml +++ b/action.yml @@ -20,6 +20,12 @@ inputs: Allow installing prerelease versions of Pester. required: false default: 'false' + Guid: + description: | + Optional module identity (GUID) that the installed Pester must match, mirroring the GUID key of a + '#Requires -Modules @{ ... }' declaration. When set, the run fails fast if the loaded Pester's GUID does + not match, guarding against a different module named Pester on the runner's module path. + required: false ReportAsJson: description: | Output generated reports in JSON format in addition to the configured format through Pester. @@ -302,6 +308,7 @@ runs: PSMODULE_INVOKE_PESTER_INPUT_Path: ${{ inputs.Path }} PSMODULE_INVOKE_PESTER_INPUT_Version: ${{ inputs.Version }} PSMODULE_INVOKE_PESTER_INPUT_Prerelease: ${{ inputs.Prerelease }} + PSMODULE_INVOKE_PESTER_INPUT_Guid: ${{ inputs.Guid }} PSMODULE_INVOKE_PESTER_INPUT_Run_Path: ${{ inputs.Run_Path }} PSMODULE_INVOKE_PESTER_INPUT_Run_ExcludePath: ${{ inputs.Run_ExcludePath }} PSMODULE_INVOKE_PESTER_INPUT_Run_ScriptBlock: ${{ inputs.Run_ScriptBlock }} @@ -365,6 +372,7 @@ runs: PSMODULE_INVOKE_PESTER_INPUT_ReportAsJson: ${{ inputs.ReportAsJson }} PSMODULE_INVOKE_PESTER_INPUT_Version: ${{ inputs.Version }} PSMODULE_INVOKE_PESTER_INPUT_Prerelease: ${{ inputs.Prerelease }} + PSMODULE_INVOKE_PESTER_INPUT_Guid: ${{ inputs.Guid }} PSMODULE_INVOKE_PESTER_INPUT_Notice_Mode: ${{ inputs.Notice_Mode }} PSMODULE_INVOKE_PESTER_INPUT_StepSummary_Mode: ${{ inputs.StepSummary_Mode }} PSMODULE_INVOKE_PESTER_INPUT_StepSummary_ShowTestOverview: ${{ inputs.StepSummary_ShowTestOverview }} diff --git a/src/Invoke-Pester.Helpers.psm1 b/src/Invoke-Pester.Helpers.psm1 index e6646a66..6ff2b6bd 100644 --- a/src/Invoke-Pester.Helpers.psm1 +++ b/src/Invoke-Pester.Helpers.psm1 @@ -1311,6 +1311,11 @@ function Install-PSResourceWithRetry { [Parameter()] [switch] $Prerelease, + # Optional module identity (GUID) the loaded module must match. When set, the imported module's GUID is + # validated and a mismatch throws, guarding against a different module with the same name. + [Parameter()] + [string] $Guid, + # Number of times to retry installation, default is 5 [Parameter()] [int] $RetryCount = 5, @@ -1383,5 +1388,15 @@ function Install-PSResourceWithRetry { } else { Import-Module -Name $Name -Force -Global -ErrorAction Stop } + + # Validate module identity when a GUID pin is supplied, so a different module with the same name cannot + # satisfy the request. This fails at install time (shifted left) in addition to any '#Requires' GUID pin. + if (-not [string]::IsNullOrWhiteSpace($Guid)) { + $loaded = Get-Module -Name $Name | Sort-Object Version -Descending | Select-Object -First 1 + if (-not $loaded -or $loaded.Guid -ne [guid]$Guid) { + throw "Loaded '$Name' does not match the required GUID '$Guid' (loaded GUID: $($loaded.Guid))." + } + Write-Output "Verified module identity for ${Name}: GUID $Guid" + } } } diff --git a/src/exec.ps1 b/src/exec.ps1 index 66b19e1c..9810aa46 100644 --- a/src/exec.ps1 +++ b/src/exec.ps1 @@ -10,7 +10,8 @@ Import-Module "$PSScriptRoot/Invoke-Pester.Helpers.psm1" # Install Pester honoring the optional version constraint from the action input. An empty value installs the latest version. $pesterVersion = $env:PSMODULE_INVOKE_PESTER_INPUT_Version $pesterPrerelease = $env:PSMODULE_INVOKE_PESTER_INPUT_Prerelease -eq 'true' -Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease +$pesterGuid = $env:PSMODULE_INVOKE_PESTER_INPUT_Guid +Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease -Guid $pesterGuid '::endgroup::' '::group::Exec - Get test kit versions' @@ -63,7 +64,7 @@ $PSStyle.OutputRendering = 'Ansi' '::group::Eval - Setup prerequisites' # Reuse the Pester version constraint resolved during Exec setup. An empty value installs the latest version. -Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease +Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease -Guid $pesterGuid 'Hashtable', 'TimeSpan', 'Markdown' | Install-PSResourceWithRetry '::endgroup::' diff --git a/src/init.ps1 b/src/init.ps1 index d1a7f25c..23186d25 100644 --- a/src/init.ps1 +++ b/src/init.ps1 @@ -6,7 +6,8 @@ LogGroup 'Init - Setup prerequisites' { # Install Pester honoring the optional version constraint from the action input. An empty value installs the latest version. $pesterVersion = $env:PSMODULE_INVOKE_PESTER_INPUT_Version $pesterPrerelease = $env:PSMODULE_INVOKE_PESTER_INPUT_Prerelease -eq 'true' - Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease + $pesterGuid = $env:PSMODULE_INVOKE_PESTER_INPUT_Guid + Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease -Guid $pesterGuid 'Hashtable', 'TimeSpan', 'Markdown' | Install-PSResourceWithRetry } From 26b847a040d71330cfa61e6abdb33270a6ba83f9 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 20:10:44 +0200 Subject: [PATCH 3/6] Add PesterGuidMatch job for the successful Guid-input path Positive counterpart to PesterGuidMismatch: passes the correct Guid via the action input with a version-only (no GUID) #Requires, isolating and proving the matching Guid input lets the run proceed and the tests pass. Wired into CatchJob needs, env vars, and Test-ActionResults.ps1. --- .github/workflows/Action-Test.yml | 38 +++++++++++++++++++ .../PesterGuidMatch/PesterVersion.Tests.ps1 | 13 +++++++ tests/Test-ActionResults.ps1 | 7 ++++ 3 files changed, 58 insertions(+) create mode 100644 tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1 diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index 955c8615..ac1815b6 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -396,6 +396,39 @@ jobs: CONCLUSION: ${{ steps.action-test.conclusion }} run: tests/Show-Status.ps1 + ActionTestPesterGuidMatch: + name: Action-Test - [Pester GUID Match] + runs-on: ubuntu-latest + outputs: + Outcome: ${{ steps.action-test.outcome }} + Conclusion: ${{ steps.action-test.conclusion }} + Executed: ${{ steps.action-test.outputs.Executed }} + Result: ${{ steps.action-test.outputs.Result }} + + steps: + # Need to check out as part of the test, as it's a local action + - name: Checkout repo + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + # A matching GUID passed via the action input must let the run proceed and the tests pass. + - name: Action-Test [Pester GUID Match] + uses: ./ + id: action-test + with: + WorkingDirectory: tests/4-PesterVersionConstraints/PesterGuidMatch + Version: '6.0.0' + Guid: 'a699dea5-2c73-4616-a270-1f7abb777e71' + TestResult_TestSuiteName: PesterGuidMatch + + - name: Status + shell: pwsh + env: + OUTCOME: ${{ steps.action-test.outcome }} + CONCLUSION: ${{ steps.action-test.conclusion }} + run: tests/Show-Status.ps1 + ActionTestPesterGuidMismatch: name: Action-Test - [Pester GUID Mismatch] runs-on: ubuntu-latest @@ -443,6 +476,7 @@ jobs: - ActionTestPester5RangeConstraint - ActionTestPester6ExactConstraint - ActionTestPesterGuidPin + - ActionTestPesterGuidMatch if: always() runs-on: ubuntu-latest steps: @@ -502,5 +536,9 @@ jobs: ACTIONTESTPESTERGUIDPIN_CONCLUSION: ${{ needs.ActionTestPesterGuidPin.outputs.Conclusion }} ACTIONTESTPESTERGUIDPIN_EXECUTED: ${{ needs.ActionTestPesterGuidPin.outputs.Executed }} ACTIONTESTPESTERGUIDPIN_RESULT: ${{ needs.ActionTestPesterGuidPin.outputs.Result }} + ACTIONTESTPESTERGUIDMATCH_OUTCOME: ${{ needs.ActionTestPesterGuidMatch.outputs.Outcome }} + ACTIONTESTPESTERGUIDMATCH_CONCLUSION: ${{ needs.ActionTestPesterGuidMatch.outputs.Conclusion }} + ACTIONTESTPESTERGUIDMATCH_EXECUTED: ${{ needs.ActionTestPesterGuidMatch.outputs.Executed }} + ACTIONTESTPESTERGUIDMATCH_RESULT: ${{ needs.ActionTestPesterGuidMatch.outputs.Result }} with: Script: tests/Test-ActionResults.ps1 diff --git a/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1 b/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1 new file mode 100644 index 00000000..caa941cd --- /dev/null +++ b/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1 @@ -0,0 +1,13 @@ +#Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '6.0.0' } + +Describe 'Pester GUID input (match)' { + It 'runs when the Guid input matches the installed Pester identity' { + # The GUID is pinned through the action's Guid input (validated at install), not via #Requires here, + # so reaching and passing this test proves the matching Guid input let the run proceed. + $module = Get-Module -Name Pester | Sort-Object Version -Descending | Select-Object -First 1 + + $module | Should -Not -Be $null + $module.Version | Should -Be ([version]'6.0.0') + $module.Guid | Should -Be ([guid]'a699dea5-2c73-4616-a270-1f7abb777e71') + } +} diff --git a/tests/Test-ActionResults.ps1 b/tests/Test-ActionResults.ps1 index 99616bec..de5e5602 100644 --- a/tests/Test-ActionResults.ps1 +++ b/tests/Test-ActionResults.ps1 @@ -115,6 +115,13 @@ $jobs = @( Executed = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_EXECUTED; Expected = 'True' } Result = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_RESULT; Expected = 'Passed' } } + @{ + Name = 'Action-Test - [Pester GUID Match]' + Outcome = @{ Actual = $env:ACTIONTESTPESTERGUIDMATCH_OUTCOME; Expected = 'success' } + Conclusion = @{ Actual = $env:ACTIONTESTPESTERGUIDMATCH_CONCLUSION; Expected = 'success' } + Executed = @{ Actual = $env:ACTIONTESTPESTERGUIDMATCH_EXECUTED; Expected = 'True' } + Result = @{ Actual = $env:ACTIONTESTPESTERGUIDMATCH_RESULT; Expected = 'Passed' } + } ) # Add Pass property to each check and convert to PSCustomObject for table output From cb8ecb4bc5548028b5226dadb03ca8fbd2bda385 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 20:19:35 +0200 Subject: [PATCH 4/6] Harden GUID validation and make GUID tests deterministic Address Copilot review on PR #73: validate the exact module returned by Import-Module -PassThru (not the highest loaded version) so a side-by-side load cannot mask a mismatch; parse the Guid with [guid]::TryParse for a clear error on malformed input. The PesterGuidPin and PesterGuidMatch tests now select the expected version explicitly instead of the highest loaded. --- src/Invoke-Pester.Helpers.psm1 | 18 ++++++++++++------ .../PesterGuidMatch/PesterVersion.Tests.ps1 | 6 +++--- .../PesterGuidPin/PesterVersion.Tests.ps1 | 6 +++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Invoke-Pester.Helpers.psm1 b/src/Invoke-Pester.Helpers.psm1 index 6ff2b6bd..3a2d0731 100644 --- a/src/Invoke-Pester.Helpers.psm1 +++ b/src/Invoke-Pester.Helpers.psm1 @@ -1378,7 +1378,7 @@ function Install-PSResourceWithRetry { # highest version available on PSModulePath. if ($resolved) { Write-Output "Importing module: $Name $($resolved.Version)" - Import-Module -Name $Name -RequiredVersion $resolved.Version -Force -Global -ErrorAction Stop + $imported = Import-Module -Name $Name -RequiredVersion $resolved.Version -Force -Global -PassThru -ErrorAction Stop } elseif (-not [string]::IsNullOrWhiteSpace($Version)) { # A version constraint was requested but no satisfying installed version could be resolved. Importing the # module unconstrained here would silently load an arbitrary copy from PSModulePath (for example the @@ -1386,17 +1386,23 @@ function Install-PSResourceWithRetry { # exists to guarantee. Fail fast instead. throw "No installed '$Name' version satisfies constraint '$Version'; refusing to import an unconstrained version." } else { - Import-Module -Name $Name -Force -Global -ErrorAction Stop + $imported = Import-Module -Name $Name -Force -Global -PassThru -ErrorAction Stop } # Validate module identity when a GUID pin is supplied, so a different module with the same name cannot # satisfy the request. This fails at install time (shifted left) in addition to any '#Requires' GUID pin. if (-not [string]::IsNullOrWhiteSpace($Guid)) { - $loaded = Get-Module -Name $Name | Sort-Object Version -Descending | Select-Object -First 1 - if (-not $loaded -or $loaded.Guid -ne [guid]$Guid) { - throw "Loaded '$Name' does not match the required GUID '$Guid' (loaded GUID: $($loaded.Guid))." + $expectedGuid = [guid]::Empty + if (-not [guid]::TryParse($Guid, [ref]$expectedGuid)) { + throw "The provided Guid '$Guid' is not a valid GUID." } - Write-Output "Verified module identity for ${Name}: GUID $Guid" + # Validate the module this call just imported (via -PassThru), not the highest version that happens + # to be loaded, so a side-by-side loaded version cannot mask a mismatch. + $importedModule = $imported | Where-Object { $_.Name -eq $Name } | Select-Object -First 1 + if (-not $importedModule -or $importedModule.Guid -ne $expectedGuid) { + throw "Loaded '$Name' does not match the required GUID '$expectedGuid' (loaded GUID: $($importedModule.Guid))." + } + Write-Output "Verified module identity for ${Name}: GUID $expectedGuid" } } } diff --git a/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1 b/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1 index caa941cd..8c7f90c5 100644 --- a/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1 +++ b/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1 @@ -4,10 +4,10 @@ Describe 'Pester GUID input (match)' { It 'runs when the Guid input matches the installed Pester identity' { # The GUID is pinned through the action's Guid input (validated at install), not via #Requires here, # so reaching and passing this test proves the matching Guid input let the run proceed. - $module = Get-Module -Name Pester | Sort-Object Version -Descending | Select-Object -First 1 + # Select the pinned version explicitly so a side-by-side loaded version cannot make the assertion flaky. + $module = Get-Module -Name Pester | Where-Object { $_.Version -eq [version]'6.0.0' } | Select-Object -First 1 - $module | Should -Not -Be $null - $module.Version | Should -Be ([version]'6.0.0') + $module | Should -Not -BeNullOrEmpty $module.Guid | Should -Be ([guid]'a699dea5-2c73-4616-a270-1f7abb777e71') } } diff --git a/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1 b/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1 index 65bc201f..db087943 100644 --- a/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1 +++ b/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1 @@ -4,10 +4,10 @@ Describe 'Pester GUID pin' { It 'loads the Pester module identified by its GUID' { # The #Requires above pins Pester by module identity (GUID) in addition to version. PowerShell refuses to # run this file unless the loaded Pester matches both, so reaching this assertion already proves the pin held. - $module = Get-Module -Name Pester | Sort-Object Version -Descending | Select-Object -First 1 + # Select the pinned version explicitly so a side-by-side loaded version cannot make the assertion flaky. + $module = Get-Module -Name Pester | Where-Object { $_.Version -eq [version]'6.0.0' } | Select-Object -First 1 - $module | Should -Not -Be $null + $module | Should -Not -BeNullOrEmpty $module.Guid | Should -Be ([guid]'a699dea5-2c73-4616-a270-1f7abb777e71') - $module.Version | Should -Be ([version]'6.0.0') } } From f87bce5d6b22d15c45c91c22a424364151561e04 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 20:31:04 +0200 Subject: [PATCH 5/6] Use Get-InstalledPSResource for the installed-version fallback Address Copilot review on PR #73: the fallback used Get-PSResource, which is the installed-resource query (alias of Get-InstalledPSResource; the gallery cmdlet is Find-PSResource). Switched to the explicit Get-InstalledPSResource so the installed-only intent is unambiguous and to avoid the alias. --- src/Invoke-Pester.Helpers.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Invoke-Pester.Helpers.psm1 b/src/Invoke-Pester.Helpers.psm1 index 3a2d0731..300f0c3e 100644 --- a/src/Invoke-Pester.Helpers.psm1 +++ b/src/Invoke-Pester.Helpers.psm1 @@ -1370,7 +1370,7 @@ function Install-PSResourceWithRetry { if (-not [string]::IsNullOrWhiteSpace($Version)) { $getParams['Version'] = $Version } - $resolved = Get-PSResource @getParams | Sort-Object Version -Descending | Select-Object -First 1 + $resolved = Get-InstalledPSResource @getParams | Sort-Object Version -Descending | Select-Object -First 1 } # Import into the global session state so the resolved version is the one every subsequent From 432cfc26a6475fb10f0a2180f2817240d5e4f435 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 20:38:55 +0200 Subject: [PATCH 6/6] Make CatchJob wait on the GUID mismatch job Address Copilot review on PR #73: add ActionTestPesterGuidMismatch to CatchJob.needs so the aggregate/step-summary runs only after the negative job completes. The negative job stays self-asserting (a wrong Guid aborts the action at init, leaving empty Executed/Result that do not fit the success-oriented results table). Clarified the Test-ActionResults description accordingly. --- .github/workflows/Action-Test.yml | 1 + tests/Test-ActionResults.ps1 | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index ac1815b6..fb63f0c5 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -477,6 +477,7 @@ jobs: - ActionTestPester6ExactConstraint - ActionTestPesterGuidPin - ActionTestPesterGuidMatch + - ActionTestPesterGuidMismatch if: always() runs-on: ubuntu-latest steps: diff --git a/tests/Test-ActionResults.ps1 b/tests/Test-ActionResults.ps1 index de5e5602..0fd34adb 100644 --- a/tests/Test-ActionResults.ps1 +++ b/tests/Test-ActionResults.ps1 @@ -1,7 +1,8 @@ <# .DESCRIPTION - Aggregates and validates test results from all Action-Test workflow jobs. - Compares actual outcomes against expected values and generates a summary report. + Aggregates and validates results from the result-producing Action-Test workflow jobs. + Compares actual outcomes against expected values and generates a summary report. The negative + 'Action-Test - [Pester GUID Mismatch]' job self-validates in its own step and is not tabled here. #> [CmdletBinding()]