test: add send audit idempotency diagnostics

This commit is contained in:
zhaoyilun
2026-07-10 20:28:13 +08:00
parent dadf42817c
commit 2a7087218b
6 changed files with 279 additions and 7 deletions

View 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
}
}

View 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
}