test: add send audit idempotency diagnostics
This commit is contained in:
49
scripts/test-verify-send-audit-idempotency.ps1
Normal file
49
scripts/test-verify-send-audit-idempotency.ps1
Normal file
@@ -0,0 +1,49 @@
|
||||
Set-StrictMode -Version 2.0
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$verifier = Join-Path $PSScriptRoot "verify-send-audit-idempotency.ps1"
|
||||
$workDir = Join-Path ([System.IO.Path]::GetTempPath()) ("isphere-send-audit-idempotency-test-" + [System.Guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $workDir | Out-Null
|
||||
|
||||
try {
|
||||
$auditPath = Join-Path $workDir "send-audit.jsonl"
|
||||
$idempotencyPath = Join-Path $workDir "send-idempotency.jsonl"
|
||||
|
||||
@(
|
||||
@{ tool = "isphere_send_message"; target_ref = "contact:alice"; content_sha256 = "hash-a"; idempotency_key_sha256 = "key-a"; result = "planned" },
|
||||
@{ tool = "isphere_send_message"; target_ref = "contact:alice"; content_sha256 = "hash-a"; idempotency_key_sha256 = "key-a"; result = "planned" },
|
||||
@{ tool = "isphere_send_message"; target_ref = "contact:alice"; content_sha256 = "hash-b"; idempotency_key_sha256 = "key-a"; result = "blocked" }
|
||||
) | ForEach-Object { $_ | ConvertTo-Json -Compress } | Set-Content -Path $auditPath -Encoding UTF8
|
||||
|
||||
@(
|
||||
@{ tool = "isphere_send_message"; target_ref = "contact:alice"; content_sha256 = "hash-a"; idempotency_key_sha256 = "key-a"; audit_ref = "send-message:first" }
|
||||
) | ForEach-Object { $_ | ConvertTo-Json -Compress } | Set-Content -Path $idempotencyPath -Encoding UTF8
|
||||
|
||||
$summary = & $verifier -AuditPath $auditPath -IdempotencyPath $idempotencyPath | ConvertFrom-Json
|
||||
if (-not $summary.ok) { throw "expected ok summary, got: $($summary | ConvertTo-Json -Compress)" }
|
||||
if ($summary.audit_records -ne 3) { throw "audit_records=$($summary.audit_records), want 3" }
|
||||
if ($summary.idempotency_records -ne 1) { throw "idempotency_records=$($summary.idempotency_records), want 1" }
|
||||
if (-not $summary.duplicate_detected) { throw "duplicate_detected should be true" }
|
||||
if (-not $summary.conflict_detected) { throw "conflict_detected should be true" }
|
||||
if ($summary.raw_body_present) { throw "raw_body_present should be false" }
|
||||
if ($summary.raw_idempotency_key_present) { throw "raw_idempotency_key_present should be false" }
|
||||
|
||||
$leakyAuditPath = Join-Path $workDir "leaky-audit.jsonl"
|
||||
@{ tool = "isphere_send_message"; content_text = "raw body"; idempotency_key_sha256 = "key-b" } | ConvertTo-Json -Compress | Set-Content -Path $leakyAuditPath -Encoding UTF8
|
||||
$leakyOutput = & $verifier -AuditPath $leakyAuditPath -IdempotencyPath $idempotencyPath 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
throw "leaky audit unexpectedly passed: $leakyOutput"
|
||||
}
|
||||
|
||||
Write-Output (@{
|
||||
ok = $true
|
||||
verification = "send_audit_idempotency_fixture"
|
||||
repo_root = $repoRoot.Path
|
||||
} | ConvertTo-Json -Compress)
|
||||
}
|
||||
finally {
|
||||
if (Test-Path $workDir) {
|
||||
Remove-Item -LiteralPath $workDir -Recurse -Force
|
||||
}
|
||||
}
|
||||
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