@@ -13,6 +13,7 @@ BeforeAll {
1313 throw ' Cannot acquire the process environment lock for action tests.'
1414 }
1515 Import-Module - Name ' PSModule' - Force
16+ Import-Module - Name ([IO.Path ]::Combine($PSScriptRoot , ' ..' , ' src' , ' Publish-PSModule.Helpers.psm1' )) - Force
1617 Import-Module - Name (Join-Path - Path $PSScriptRoot - ChildPath ' Publish-PSModule.TestFixture.psm1' ) - Force
1718
1819 $script :publishScriptPath = [IO.Path ]::GetFullPath(
@@ -178,6 +179,43 @@ Describe 'Publish-PSModule source-bound preflight' {
178179 $failure.Exception.Message | Should -Match ' 503|API|read|lookup'
179180 }
180181
182+ It ' rejects a malformed <Resource> response before publication' - ForEach @ (
183+ @ { Resource = ' repository array' }
184+ @ { Resource = ' commit array' }
185+ @ { Resource = ' string write permission' }
186+ ) {
187+ if ($Resource -ceq ' commit array' ) {
188+ $endpoint = " repos/PSModule/TestModule/git/commits/$env: GITHUB_SHA "
189+ $script :state.ApiResponses [$endpoint ] = @ ([pscustomobject ]@ { sha = $env: GITHUB_SHA })
190+ } else {
191+ $metadata = [pscustomobject ]@ {
192+ full_name = ' PSModule/TestModule' ; permissions = [pscustomobject ]@ { push = $true }
193+ }
194+ if ($Resource -ceq ' string write permission' ) {
195+ $metadata.permissions.push = ' true'
196+ $script :state.ApiResponses [' repos/PSModule/TestModule' ] = $metadata
197+ } else {
198+ $script :state.ApiResponses [' repos/PSModule/TestModule' ] = @ ($metadata )
199+ }
200+ }
201+
202+ $failure = $null
203+ try { & $script :publishScriptPath 1> $null 6> $null } catch { $failure = $_ }
204+
205+ $script :state.GalleryWrites | Should - Be 0
206+ $script :state.GitHubWrites | Should - Be 0
207+ $failure.Exception.Message | Should -Match ' metadata|access|source'
208+ }
209+
210+ It ' retains a verified empty tag enumeration instead of treating it as an unknown JSON response' {
211+ $script :state.ApiResponses [' repos/PSModule/TestModule/git/matching-refs/tags/' ] = @ ()
212+
213+ $response = Invoke-PSModulePublicationApi - Endpoint ' repos/PSModule/TestModule/git/matching-refs/tags/'
214+
215+ $response.Value -is [array ] | Should - BeTrue
216+ $response.Value.Count | Should - Be 0
217+ }
218+
181219 It ' rejects a Gallery-only package with no source and artifact proof' {
182220 $script :state.Package = [pscustomobject ]@ { Name = ' TestModule' ; Version = ' 1.2.4' }
183221
@@ -475,6 +513,75 @@ Describe 'Publish-PSModule source-bound preflight' {
475513 $script :state.GitHubWrites | Should - Be 0
476514 }
477515
516+ It ' rejects a terminal ownership marker with <Shape> before publication' - ForEach @ (
517+ @ { Shape = ' indentation' }
518+ @ { Shape = ' inline placement' }
519+ @ { Shape = ' missing delimiter spacing' }
520+ ) {
521+ Set-PSModuleFixtureClaim - State $script :state - Claim $script :state.ExpectedClaim
522+ $owner = [pscustomobject ]@ {
523+ Repository = ' Other/TestModule' ; PullRequest = 99 ; CommitSha = $env: GITHUB_SHA
524+ } | ConvertTo-Json - Compress
525+ $encoded = [Convert ]::ToBase64String([Text.Encoding ]::UTF8.GetBytes($owner ))
526+ $footer = " <!-- PSModule.PrereleaseOwner:v1:$encoded -->"
527+ if ($Shape -ceq ' indentation' ) {
528+ $body = " Notes.`n $footer "
529+ } elseif ($Shape -ceq ' inline placement' ) {
530+ $body = " Notes. $footer "
531+ } else {
532+ $body = $footer.Replace (' <!-- ' , ' <!--' )
533+ }
534+ $script :state.Release = [pscustomobject ]@ {
535+ tag_name = ' v1.2.4' ; html_url = ' https://github.com/PSModule/TestModule/releases/tag/v1.2.4'
536+ prerelease = $false ; draft = $false ; body = $body ; assets = @ ()
537+ }
538+
539+ $failure = $null
540+ try { & $script :publishScriptPath 1> $null 6> $null } catch { $failure = $_ }
541+
542+ $script :state.GalleryWrites | Should - Be 0
543+ $script :state.GitHubWrites | Should - Be 0
544+ $failure.Exception.Message | Should -Match ' footer'
545+ }
546+
547+ It ' rejects a stable PR release with no producing-context footer rather than inferring ownership from its tag' {
548+ $script :state.ExpectedClaim.PullRequest = 42
549+ $env: PSMODULE_PUBLISH_PSMODULE_INPUT_PullRequest = ' {"Number":42}'
550+ Set-PSModuleFixtureClaim - State $script :state - Claim $script :state.ExpectedClaim
551+ $script :state.Release = [pscustomobject ]@ {
552+ tag_name = ' v1.2.4' ; html_url = ' https://github.com/PSModule/TestModule/releases/tag/v1.2.4'
553+ prerelease = $false ; draft = $false ; body = ' Unknown stable publication context.' ; assets = @ ()
554+ }
555+
556+ $failure = $null
557+ try { & $script :publishScriptPath 1> $null 6> $null } catch { $failure = $_ }
558+
559+ $script :state.GalleryWrites | Should - Be 0
560+ $script :state.GitHubWrites | Should - Be 0
561+ $failure.Exception.Message | Should -Match ' footer'
562+ }
563+
564+ It ' retains complete stable PR notes and footer across an exact missing-upload retry' {
565+ $script :state.ExpectedClaim.PullRequest = 42
566+ $env: PSMODULE_RELEASE_PSMODULE_INPUT_PullRequest = ' {"Number":42,"Title":"Stable release","Body":"Complete authored stable notes."}'
567+ $env: PSMODULE_RELEASE_PSMODULE_INPUT_UsePRBodyAsReleaseNotes = ' true'
568+ $env: PSMODULE_RELEASE_PSMODULE_INPUT_UsePRTitleAsNotesHeading = ' true'
569+
570+ { & $script :releaseScriptPath 1> $null 6> $null } | Should -Not - Throw
571+
572+ $script :state.Release.body | Should -Match ' Complete authored stable notes\.'
573+ (Get-PSModulePrereleaseOwnership - Body $script :state.Release.body ).PullRequest | Should - Be 42
574+ $originalNotes = $script :state.Release.body
575+ $script :state.Release.assets = @ ()
576+ $script :state.GitHubWrites = 0
577+ $script :state.GitHubCreationUpload = 0
578+
579+ { & $script :releaseScriptPath 1> $null 6> $null } | Should -Not - Throw
580+
581+ $script :state.GitHubCreationUpload | Should - Be 1
582+ $script :state.Release.body | Should - BeExactly $originalNotes
583+ }
584+
478585 It ' rejects a disappeared previously claimed tag rather than recreating it' {
479586 Set-PSModuleFixtureClaim - State $script :state - Claim $script :state.ExpectedClaim
480587 $script :state.OnTagRead = {
0 commit comments