test: add send audit idempotency diagnostics
This commit is contained in:
155
scripts/verify-send-audit-idempotency.ps1
Normal file
155
scripts/verify-send-audit-idempotency.ps1
Normal file
@@ -0,0 +1,155 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$AuditPath,
|
||||
[Parameter(Mandatory = $true)][string]$IdempotencyPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 2.0
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Read-JsonLines {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path)) {
|
||||
return @()
|
||||
}
|
||||
$records = @()
|
||||
$lineNo = 0
|
||||
foreach ($line in Get-Content -LiteralPath $Path) {
|
||||
$lineNo++
|
||||
$trimmed = $line.Trim()
|
||||
if ($trimmed.Length -eq 0) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
$records += ($trimmed | ConvertFrom-Json)
|
||||
}
|
||||
catch {
|
||||
throw "failed to parse JSONL $Path line ${lineNo}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
return @($records)
|
||||
}
|
||||
|
||||
function Get-JsonPropertyNames {
|
||||
param([AllowNull()]$Value)
|
||||
$names = @()
|
||||
if ($null -eq $Value) {
|
||||
return $names
|
||||
}
|
||||
if ($Value -is [System.Array]) {
|
||||
foreach ($item in $Value) {
|
||||
$names += Get-JsonPropertyNames -Value $item
|
||||
}
|
||||
return $names
|
||||
}
|
||||
if ($Value -is [System.Collections.IDictionary]) {
|
||||
foreach ($key in $Value.Keys) {
|
||||
$names += [string]$key
|
||||
$names += Get-JsonPropertyNames -Value $Value[$key]
|
||||
}
|
||||
return $names
|
||||
}
|
||||
if ($Value.PSObject -and $Value.PSObject.Properties) {
|
||||
foreach ($property in $Value.PSObject.Properties) {
|
||||
$names += $property.Name
|
||||
$names += Get-JsonPropertyNames -Value $property.Value
|
||||
}
|
||||
}
|
||||
return $names
|
||||
}
|
||||
|
||||
function Get-StringProperty {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]$Record,
|
||||
[Parameter(Mandatory = $true)][string]$Name
|
||||
)
|
||||
$property = $Record.PSObject.Properties[$Name]
|
||||
if ($null -eq $property -or $null -eq $property.Value) {
|
||||
return ""
|
||||
}
|
||||
return [string]$property.Value
|
||||
}
|
||||
|
||||
$auditRecords = @(Read-JsonLines -Path $AuditPath)
|
||||
$idempotencyRecords = @(Read-JsonLines -Path $IdempotencyPath)
|
||||
$allRecords = @($auditRecords + $idempotencyRecords)
|
||||
$propertyNames = @()
|
||||
foreach ($record in $allRecords) {
|
||||
$propertyNames += Get-JsonPropertyNames -Value $record
|
||||
}
|
||||
|
||||
$rawBodyKeys = @("content_text", "ContentText", "message_body", "raw_body", "raw_content_text")
|
||||
$rawIdempotencyKeys = @("idempotency_key", "IdempotencyKey", "raw_idempotency_key")
|
||||
$rawBodyPresent = $false
|
||||
$rawIdempotencyKeyPresent = $false
|
||||
foreach ($name in $propertyNames) {
|
||||
foreach ($rawName in $rawBodyKeys) {
|
||||
if ([string]::Equals($name, $rawName, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$rawBodyPresent = $true
|
||||
}
|
||||
}
|
||||
foreach ($rawName in $rawIdempotencyKeys) {
|
||||
if ([string]::Equals($name, $rawName, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$rawIdempotencyKeyPresent = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$byIdempotencyKey = @{}
|
||||
foreach ($record in $auditRecords) {
|
||||
$keyHash = Get-StringProperty -Record $record -Name "idempotency_key_sha256"
|
||||
if ([string]::IsNullOrWhiteSpace($keyHash)) {
|
||||
continue
|
||||
}
|
||||
$fingerprint = (Get-StringProperty -Record $record -Name "target_ref") + "|" + (Get-StringProperty -Record $record -Name "content_sha256")
|
||||
if (-not $byIdempotencyKey.ContainsKey($keyHash)) {
|
||||
$byIdempotencyKey[$keyHash] = @()
|
||||
}
|
||||
$byIdempotencyKey[$keyHash] = @($byIdempotencyKey[$keyHash] + $fingerprint)
|
||||
}
|
||||
|
||||
$duplicateDetected = $false
|
||||
$conflictDetected = $false
|
||||
foreach ($keyHash in $byIdempotencyKey.Keys) {
|
||||
$fingerprints = @($byIdempotencyKey[$keyHash])
|
||||
if ($fingerprints.Count -lt 2) {
|
||||
continue
|
||||
}
|
||||
$unique = @($fingerprints | Sort-Object -Unique)
|
||||
if ($unique.Count -eq 1) {
|
||||
$duplicateDetected = $true
|
||||
}
|
||||
else {
|
||||
$conflictDetected = $true
|
||||
$counts = @{}
|
||||
foreach ($fingerprint in $fingerprints) {
|
||||
if (-not $counts.ContainsKey($fingerprint)) {
|
||||
$counts[$fingerprint] = 0
|
||||
}
|
||||
$counts[$fingerprint]++
|
||||
}
|
||||
foreach ($count in $counts.Values) {
|
||||
if ($count -gt 1) {
|
||||
$duplicateDetected = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ok = -not ($rawBodyPresent -or $rawIdempotencyKeyPresent)
|
||||
$summary = [ordered]@{
|
||||
ok = $ok
|
||||
verification = "send_audit_idempotency"
|
||||
audit_records = $auditRecords.Count
|
||||
idempotency_records = $idempotencyRecords.Count
|
||||
duplicate_detected = $duplicateDetected
|
||||
conflict_detected = $conflictDetected
|
||||
raw_body_present = $rawBodyPresent
|
||||
raw_idempotency_key_present = $rawIdempotencyKeyPresent
|
||||
audit_path_configured = -not [string]::IsNullOrWhiteSpace($AuditPath)
|
||||
idempotency_path_configured = -not [string]::IsNullOrWhiteSpace($IdempotencyPath)
|
||||
}
|
||||
|
||||
Write-Output ($summary | ConvertTo-Json -Compress)
|
||||
if (-not $ok) {
|
||||
exit 1
|
||||
}
|
||||
Reference in New Issue
Block a user