Private/QAOpsMaintainers.ps1
|
function ConvertTo-QAOpsKeyComponent { <# Percent-encodes the v1 canonical-key variable component characters that can collide with grammar tokens. #> [CmdletBinding()] param([Parameter()][AllowNull()][string]$Value) if ($null -eq $Value) { return '' } $builder = New-Object System.Text.StringBuilder for ($i = 0; $i -lt $Value.Length; $i++) { $ch = $Value[$i] $code = [int][char]$ch if ([char]::IsHighSurrogate($ch)) { if (($i + 1) -ge $Value.Length -or -not [char]::IsLowSurrogate($Value[$i + 1])) { throw "Canonical key component contains a lone surrogate." } [void]$builder.Append($ch) $i++ [void]$builder.Append($Value[$i]) continue } if ([char]::IsLowSurrogate($ch)) { throw "Canonical key component contains a lone surrogate." } if ($ch -eq '%' -or $ch -eq '|' -or [char]::IsControl($ch)) { foreach ($byte in [System.Text.Encoding]::UTF8.GetBytes([string]$ch)) { [void]$builder.Append(('%{0:X2}' -f $byte)) } } else { [void]$builder.Append($ch) } } return $builder.ToString() } function Get-QAOpsScalarCount { param([Parameter()][AllowNull()][string]$Value) if ($null -eq $Value) { return 0 } $count = 0 for ($i = 0; $i -lt $Value.Length; $i++) { if ([char]::IsHighSurrogate($Value[$i]) -and ($i + 1) -lt $Value.Length -and [char]::IsLowSurrogate($Value[$i + 1])) { $i++ } $count++ } return $count } function Get-QAOpsScalarPrefix { param([Parameter()][AllowNull()][string]$Value,[Parameter(Mandatory=$true)][int]$MaxScalars) if ($null -eq $Value -or $MaxScalars -le 0) { return '' } $count = 0 for ($i = 0; $i -lt $Value.Length; $i++) { $next = $i + 1 if ([char]::IsHighSurrogate($Value[$i]) -and $next -lt $Value.Length -and [char]::IsLowSurrogate($Value[$next])) { $i++ } $count++ if ($count -ge $MaxScalars) { return $Value.Substring(0, $i + 1) } } return $Value } function ConvertTo-QAOpsBase64Url { param([Parameter(Mandatory=$true)][byte[]]$Bytes) return ([Convert]::ToBase64String($Bytes).TrimEnd('=').Replace('+','-').Replace('/','_')) } function New-QAOpsTestInvocationIdentity { <# Builds the canonical runtime sidecar key using producer-transport-v1 §9.2 percent-encoding and shortening. #> [CmdletBinding()] param([Parameter(Mandatory=$true)][string]$Assembly,[Parameter(Mandatory=$true)][string]$FullyQualifiedName,[Parameter()][AllowNull()][string]$DataCaseId,[Parameter()][AllowNull()][string]$Target) try { $namespace = ConvertTo-QAOpsKeyComponent -Value $Assembly $identity = ConvertTo-QAOpsKeyComponent -Value $FullyQualifiedName $case = ConvertTo-QAOpsKeyComponent -Value $(if ($null -eq $DataCaseId) { '' } else { [string]$DataCaseId }) $t = ConvertTo-QAOpsKeyComponent -Value $(if ([string]::IsNullOrWhiteSpace($Target)) { 'default' } else { [string]$Target }) } catch { return [pscustomobject]@{ TestInvocationId=$null; Diagnostics=@([pscustomobject]@{ code='identity-invalid' }) } } $full = ('{0}|{1}|data:{2}|target:{3}' -f $namespace,$identity,$case,$t) $diagnostics = @() if ((Get-QAOpsScalarCount -Value $full) -gt 1024) { $sha = [System.Security.Cryptography.SHA256]::Create() try { $hash = $sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($full)) } finally { $sha.Dispose() } $hashText = ConvertTo-QAOpsBase64Url -Bytes $hash $fixedLength = (Get-QAOpsScalarCount -Value ($namespace + '||hash:' + $hashText + '|target:' + $t)) if ($fixedLength -gt 1024) { return [pscustomobject]@{ TestInvocationId=$null; Diagnostics=@([pscustomobject]@{ code='identity-invalid' }) } } $prefixBudget = [Math]::Max(0, [Math]::Min(128, 1024 - $fixedLength)) $identity = Get-QAOpsScalarPrefix -Value $identity -MaxScalars $prefixBudget $full = ('{0}|{1}|hash:{2}|target:{3}' -f $namespace,$identity,$hashText,$t) $diagnostics = @([pscustomobject]@{ code='identity-shortened' }) } [pscustomobject]@{ TestInvocationId=$full; Diagnostics=$diagnostics } } function New-QAOpsTestInvocationId { <# Back-compat string helper for the canonical runtime sidecar key. #> [CmdletBinding()] param([Parameter(Mandatory=$true)][string]$Assembly,[Parameter(Mandatory=$true)][string]$FullyQualifiedName,[Parameter()][AllowNull()][string]$DataCaseId,[Parameter()][AllowNull()][string]$Target) return (New-QAOpsTestInvocationIdentity -Assembly $Assembly -FullyQualifiedName $FullyQualifiedName -DataCaseId $DataCaseId -Target $Target).TestInvocationId } function New-QAOpsOrdinalDictionary { New-Object 'System.Collections.Generic.Dictionary[string,object]' ([System.StringComparer]::Ordinal) } function New-QAOpsSidecarImportResult { param([string]$Path,[string]$Code,[string]$Message,[switch]$Warn) if ($Warn -and $Message) { Write-Warning $Message } $diag = @() if ($Code) { $diag = @([pscustomobject]@{ code = $Code; message = $Message }) } [pscustomobject]@{ Entries = (New-QAOpsOrdinalDictionary); Diagnostics = $diag; Path = $Path } } function Test-QAOpsMaintainerEnvelope { <# Conservative non-throwing local validation of the v1 maintainer envelope before passing it to Q. #> [CmdletBinding()] param([Parameter()][object]$Maintainers) try { if ($null -eq $Maintainers -or -not ($Maintainers -is [psobject])) { return [pscustomobject]@{ Valid = $false; Code = 'missing'; Json = $null } } $props = @($Maintainers.PSObject.Properties.Name) if ($props -contains 'serializedUtf8Bytes') { $bytes = 0 if (-not [int]::TryParse([string]$Maintainers.serializedUtf8Bytes, [ref]$bytes) -or $bytes -gt 8192) { return [pscustomobject]@{ Valid = $false; Code = 'maintainersOversized'; Json = $null } } } $version = 0 if (($props -notcontains 'version') -or -not [int]::TryParse([string]$Maintainers.version, [ref]$version) -or $version -ne 1) { return [pscustomobject]@{ Valid = $false; Code = 'invalidVersion'; Json = $null } } if ($props -notcontains 'references') { return [pscustomobject]@{ Valid = $false; Code = 'invalidReference'; Json = $null } } $refs = @($Maintainers.references) if ($refs.Count -gt 20) { return [pscustomobject]@{ Valid = $false; Code = 'tooManyReferences'; Json = $null } } foreach ($ref in $refs) { if ($null -eq $ref -or @('user','team','emailContact') -notcontains [string]$ref.kind) { return [pscustomobject]@{ Valid = $false; Code = 'invalidReference'; Json = $null } } } $json = $Maintainers | ConvertTo-Json -Depth 20 -Compress if ([System.Text.Encoding]::UTF8.GetByteCount($json) -gt 8192) { return [pscustomobject]@{ Valid = $false; Code = 'maintainersOversized'; Json = $null } } [pscustomobject]@{ Valid = $true; Code = $null; Json = $json } } catch { [pscustomobject]@{ Valid = $false; Code = 'invalidReference'; Json = $null } } } function Import-QAOpsMaintainerSidecar { <# Loads the runtime sidecar once; invalid/unsupported sidecars produce no maintainers and one bounded diagnostic. #> [CmdletBinding()] param([Parameter(Mandatory=$true)][string]$ContentPath) $path = Join-Path (Join-Path (Join-Path $ContentPath 'TestHarvesting') 'dependencies.generated') 'qaops.maintainers.json' if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Write-Information "Maintainer sidecar not found: $path"; return (New-QAOpsSidecarImportResult -Path $path) } try { $item = Get-Item -LiteralPath $path -ErrorAction Stop if ($item.Length -gt 10485760) { return (New-QAOpsSidecarImportResult -Path $path -Code 'sidecar-oversized' -Message "Maintainer sidecar is oversized and will be ignored: $path" -Warn) } $doc = Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop if ($null -eq $doc -or @($doc.PSObject.Properties.Name) -notcontains 'schema' -or [string]$doc.schema -ne 'https://schema.skyline.be/qaops/maintainers/v1') { return (New-QAOpsSidecarImportResult -Path $path -Code 'sidecar-unsupported' -Message 'Maintainer sidecar schema is unsupported and will be ignored.' -Warn) } $version = 0 if (@($doc.PSObject.Properties.Name) -notcontains 'version' -or -not [int]::TryParse([string]$doc.version, [ref]$version) -or $version -ne 1) { return (New-QAOpsSidecarImportResult -Path $path -Code 'sidecar-unsupported' -Message 'Maintainer sidecar version is unsupported and will be ignored.' -Warn) } $map = New-QAOpsOrdinalDictionary foreach ($entry in @($doc.testInvocations)) { try { if ($null -eq $entry) { continue } $entryProps = @($entry.PSObject.Properties.Name) $key = $null if ($entryProps -contains 'testInvocationId') { $key = [string]$entry.testInvocationId } elseif (($entryProps -contains 'assembly') -and ($entryProps -contains 'fullyQualifiedName')) { $key = New-QAOpsTestInvocationId -Assembly ([string]$entry.assembly) -FullyQualifiedName ([string]$entry.fullyQualifiedName) -DataCaseId ([string]$entry.dataCaseId) -Target ([string]$entry.target) } if ([string]::IsNullOrWhiteSpace($key)) { continue } if (-not $map.ContainsKey($key)) { $map.Add($key, (New-Object System.Collections.ArrayList)) } [void]$map[$key].Add($entry) } catch { continue } } [pscustomobject]@{ Entries=$map; Diagnostics=@(); Path=$path } } catch { New-QAOpsSidecarImportResult -Path $path -Code 'sidecar-invalid' -Message ("Maintainer sidecar is invalid and will be ignored: {0}" -f $_.Exception.Message) -Warn } } function Resolve-QAOpsRuntimeMaintainers { <# Implements §9.2 lookup order without display-name fallback; conflicts at the same specificity return no maintainers plus a diagnostic. #> [CmdletBinding()] param([Parameter(Mandatory=$true)][object]$Entries,[Parameter(Mandatory=$true)][string]$Assembly,[Parameter(Mandatory=$true)][string]$FullyQualifiedName,[Parameter()][string]$DataCaseId,[Parameter()][string]$Target) $identity = New-QAOpsTestInvocationIdentity -Assembly $Assembly -FullyQualifiedName $FullyQualifiedName -DataCaseId $DataCaseId -Target $Target $testInvocationId = $identity.TestInvocationId if ([string]::IsNullOrEmpty($testInvocationId)) { return [pscustomobject]@{ TestInvocationId=$null; MatchedKey=$null; Maintainers=$null; Diagnostics=@($identity.Diagnostics) } } $keys = @( $testInvocationId, (New-QAOpsTestInvocationId -Assembly $Assembly -FullyQualifiedName $FullyQualifiedName -DataCaseId $DataCaseId -Target 'default'), (New-QAOpsTestInvocationId -Assembly $Assembly -FullyQualifiedName $FullyQualifiedName -DataCaseId '' -Target $Target), (New-QAOpsTestInvocationId -Assembly $Assembly -FullyQualifiedName $FullyQualifiedName -DataCaseId '' -Target 'default') ) foreach ($key in $keys) { if ($Entries.ContainsKey($key)) { $matches = @($Entries[$key]) if ($matches.Count -gt 1) { return [pscustomobject]@{ TestInvocationId=$testInvocationId; MatchedKey=$null; Maintainers=$null; Diagnostics=(@($identity.Diagnostics) + @([pscustomobject]@{ code='maintainer-conflict'; key=$key })) } } return [pscustomobject]@{ TestInvocationId=$testInvocationId; MatchedKey=$key; Maintainers=$matches[0].maintainers; Diagnostics=@($identity.Diagnostics) } } } [pscustomobject]@{ TestInvocationId=$testInvocationId; MatchedKey=$null; Maintainers=$null; Diagnostics=(@($identity.Diagnostics) + @([pscustomobject]@{ code='maintainers-not-found' })) } } |