Files
isphere-ai-bridge/scripts/verify-send-audit-idempotency.ps1
2026-07-10 20:28:13 +08:00

156 lines
4.7 KiB
PowerShell

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
}