@@ -95,6 +95,63 @@ BeforeAll {
9595 Count = [long ] $Matches.WorkCount
9696 }
9797 }
98+
99+ function Test-RemoveYamlFingerprintCollision {
100+ <#
101+ . SYNOPSIS
102+ Forces representation-key fingerprint candidates through exact graph equality.
103+ #>
104+ param (
105+ [Parameter (Mandatory )]
106+ [string ] $Yaml
107+ )
108+
109+ $implementation = {
110+ param ([string ] $YamlText )
111+
112+ $document = (Read-YamlStreamCore - Yaml $YamlText - Depth 100 - MaxNodes 100 `
113+ - MaxAliases 100 - MaxScalarLength 1048576 - MaxTagLength 1024 `
114+ - MaxTotalTagLength 65536 - MaxNumericLength 4096 ).Value[0 ]
115+ $fingerprintCache = [System.Collections.Generic.Dictionary [int , string ]]::new()
116+ foreach ($entry in $document.Entries ) {
117+ $fingerprintCache [$entry.Key.Id ] = ' forced-collision'
118+ }
119+ $hasher = [System.Security.Cryptography.SHA256 ]::Create()
120+ try {
121+ $equalityState = [pscustomobject ]@ {
122+ MaxNodes = 100
123+ FingerprintHasher = $hasher
124+ WorkState = [pscustomobject ]@ { Count = 0L ; MaxNodes = 100 }
125+ MutationState = [pscustomobject ]@ { Version = 0L }
126+ IndexDependents = (
127+ [System.Collections.Generic.Dictionary [int , object ]]::new()
128+ )
129+ Cache = (
130+ [System.Collections.Generic.Dictionary [string , bool ]]::new(
131+ [System.StringComparer ]::Ordinal
132+ )
133+ )
134+ InputIndex = 0
135+ }
136+ Test-YamlNodeGraph - Node $document `
137+ - Visited ([System.Collections.Generic.HashSet [int ]]::new()) `
138+ - FingerprintCache $fingerprintCache - FingerprintHasher $hasher `
139+ - EqualityState $equalityState `
140+ - EqualityFingerprintCache (
141+ [System.Collections.Generic.Dictionary [int , string ]]::new()
142+ )
143+ } finally {
144+ $hasher.Dispose ()
145+ }
146+ return $true
147+ }
148+
149+ $loadedModule = Get-Module - Name Yaml | Select-Object - First 1
150+ if ($null -eq $loadedModule ) {
151+ return & $implementation $Yaml
152+ }
153+ return & $loadedModule $implementation $Yaml
154+ }
98155}
99156
100157Describe ' Remove-YamlEntry' {
@@ -781,6 +838,25 @@ ordered: !!omap
781838 $failure.FullyQualifiedErrorId |
782839 Should - Be ' YamlDuplicateKey,Remove-YamlEntry'
783840 }
841+
842+ It ' confirms fingerprint candidates with exact graph equality' - ForEach @ (
843+ @ {
844+ Yaml = @'
845+ ? { x: 1 }
846+ : first
847+ ? { x: 2 }
848+ : second
849+ '@
850+ }
851+ @ {
852+ Yaml = @'
853+ !!str key: string
854+ !<tag:yaml.org,2002:st%C2%ADr> key: tagged
855+ '@
856+ }
857+ ) {
858+ Test-RemoveYamlFingerprintCollision - Yaml $Yaml | Should - BeTrue
859+ }
784860 }
785861
786862 Context ' Validation and work limits' {
@@ -819,6 +895,27 @@ ordered: !!omap
819895 $failure.Exception.Data [' YamlRemovalWorkLimit' ] | Should - Be 5
820896 }
821897
898+ It ' bounds duplicate-key graph equality with removal error classification' {
899+ $pairs = 0 .. 9 | ForEach-Object { " key$ ( $_ ) : $ ( $_ ) " }
900+ $shared = @ (' shared: &shared' ) + $pairs + ' drop: true'
901+ $literal = ($pairs | ForEach-Object Trim) -join ' , '
902+ $yaml = (@ ($shared ) + ' ? *shared' + ' : first' +
903+ " ? { $literal }" + ' : second' ) -join " `n "
904+ $failure = Get-RemoveYamlFailure {
905+ Remove-YamlEntry $yaml ' /shared/drop' - MaxNodes 80
906+ }
907+
908+ $failure.Exception.Data [' YamlErrorId' ] |
909+ Should - BeExactly ' YamlRemovalWorkLimitExceeded'
910+ $failure.FullyQualifiedErrorId |
911+ Should - Be ' YamlRemovalWorkLimitExceeded,Remove-YamlEntry'
912+ $failure.Exception.Data [' YamlRemovalWorkLimit' ] | Should - Be 80
913+ $failure.Exception.Data [' YamlRemovalWorkOperation' ] |
914+ Should - BeExactly ' duplicate-key graph comparison'
915+ $failure.Exception.Message |
916+ Should -Match ' duplicate-key graph comparison'
917+ }
918+
822919 It ' reports deterministic work and produces a result within budget' {
823920 $measurement = Measure-RemoveYamlWork `
824921 - Yaml " root:`n first: 1`n second: 2`n third: 3" `
0 commit comments