Files
story/scripts/smoke-consent-withdrawal-stop.ps1
T

146 lines
9.5 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$root = Split-Path -Parent $PSScriptRoot
Set-Location -LiteralPath $root
$api = 'http://localhost:58080'
$tokenUrl = 'http://localhost:58180/realms/voice-local/protocol/openid-connect/token'
$deviceId = '10000000-0000-0000-0000-000000000511'
$subjectId = '10000000-0000-0000-0000-000000000411'
$subjectInternalId = '10000000-0000-0000-0000-000000000401'
$consentId = '10000000-0000-0000-0000-000000000812'
$deviceSecret = [Text.Encoding]::UTF8.GetBytes('local-simulated-device-secret-32-bytes-minimum')
$conversationId = $null
$withdrawalSnapshotId = $null
$originalConsentVersion = $null
$withdrawRequestId = 'consent-stop-smoke-' + [Guid]::NewGuid().ToString('N')
function Scalar([string]$Sql) {
$value = docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -t -A -c $Sql
if ($LASTEXITCODE -ne 0) { throw '数据库查询失败。' }
return ($value | Out-String).Trim()
}
function Token() {
return (Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'password'; client_id = 'voice-platform-web'; username = 'owner'
password = 'LocalOnly123!'; scope = 'openid'
}).access_token
}
function SignedPost([string]$Path, [string]$Body, [string]$Key) {
$bytes = [Text.Encoding]::UTF8.GetBytes($Body)
$digest = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant()
$timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
$nonce = [Guid]::NewGuid().ToString('N')
$canonical = "POST`n$Path`n$timestamp`n$nonce`n$digest"
$hmac = [Security.Cryptography.HMACSHA256]::new($deviceSecret)
try {
$signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical))).TrimEnd('=').Replace('+', '-').Replace('/', '_')
} finally {
$hmac.Dispose()
}
$headers = @{
'X-Device-Id' = $deviceId; 'X-Device-Timestamp' = $timestamp; 'X-Device-Nonce' = $nonce
'X-Device-Content-SHA256' = $digest; 'X-Device-Signature' = $signature
}
if ($Key) { $headers['Idempotency-Key'] = $Key }
return Invoke-WebRequest -UseBasicParsing -Method Post -Uri ($api + $Path) -ContentType 'application/json' -Body $Body -Headers $headers
}
try {
if ((Scalar "SELECT count(*) FROM conversation c JOIN device d ON d.id=c.device_id WHERE d.public_id='$deviceId'::uuid AND c.status NOT IN('COMPLETED','FAILED','EXPIRED')") -ne '0') {
throw '测试设备存在未结束通话。'
}
$originalConsentVersion = Scalar "SELECT version FROM consent_record WHERE public_id='$consentId'::uuid AND withdrawn_at IS NULL"
if (-not $originalConsentVersion) { throw '故事整理授权不是有效状态。' }
$createBody = @{
device_time = [DateTime]::UtcNow.ToString('o'); monotonic_ms = 51001
capabilities = @{ rtc = $true; audio_tracks = @('near', 'far'); upload_resume = $true }
network = @{ type = 'wifi'; rssi = -48 }; requested_goal = 'open_conversation'
} | ConvertTo-Json -Compress -Depth 6
$created = ((SignedPost '/device/v2/conversations' $createBody ('consent-stop-create-' + [Guid]::NewGuid().ToString('N'))).Content | ConvertFrom-Json).data
$conversationId = [string]$created.conversation_id
$activatePath = "/device/v2/conversations/$conversationId/activate"
$activateBody = @{
joined_at = [DateTime]::UtcNow.ToString('o'); room_id = $created.rtc.room_id
rtc_user_id = $created.rtc.user_id; rtc_join_result = 'success'; config_revision = 1
connection_reference = 'consent-withdrawal-smoke'
} | ConvertTo-Json -Compress
SignedPost $activatePath $activateBody ('consent-stop-activate-' + [Guid]::NewGuid().ToString('N')) | Out-Null
$deadline = [DateTime]::UtcNow.AddSeconds(15)
do {
Start-Sleep -Milliseconds 300
$state = Scalar "SELECT status FROM conversation WHERE public_id='$conversationId'::uuid"
} while ($state -ne 'ACTIVE' -and [DateTime]::UtcNow -lt $deadline)
if ($state -ne 'ACTIVE') { throw "通话没有进入ACTIVE:$state" }
$accessToken = Token
$withdraw = Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/subjects/$subjectId/consents/$consentId/withdraw" `
-Headers @{ Authorization = "Bearer $accessToken"; 'X-Request-Id' = $withdrawRequestId }
if ($withdraw.StatusCode -ne 200 -or -not (($withdraw.Content | ConvertFrom-Json).data.withdrawn_at)) { throw '授权撤回接口未成功。' }
$withdrawalSnapshotId = Scalar "SELECT cs.public_id::text FROM consent_snapshot cs WHERE cs.subject_id='$subjectInternalId'::uuid AND cs.id<>(SELECT consent_snapshot_id FROM conversation WHERE public_id='$conversationId'::uuid) ORDER BY cs.created_at DESC,cs.id DESC LIMIT 1"
$stopEvidence = Scalar "SELECT c.stop_reason||'|'||(SELECT count(*) FROM conversation_state_history h WHERE h.conversation_id=c.id AND h.from_status='ACTIVE' AND h.to_status='STOPPING')||'|'||(SELECT count(*) FROM outbox_event o WHERE o.aggregate_id=c.id AND o.event_type='conversation.agent_stop_requested') FROM conversation c WHERE c.public_id='$conversationId'::uuid"
if ($stopEvidence -ne 'consent_withdrawn:story_extraction|1|1') { throw "授权撤回停止意图不完整:$stopEvidence" }
$deadline = [DateTime]::UtcNow.AddSeconds(15)
do {
Start-Sleep -Milliseconds 300
$finalState = Scalar "SELECT c.status||'|'||rt.status||'|'||rt.stop_attempts FROM conversation c JOIN rtc_task rt ON rt.conversation_id=c.id WHERE c.public_id='$conversationId'::uuid"
} while ($finalState -ne 'COMPLETED|COMPLETED|1' -and [DateTime]::UtcNow -lt $deadline)
if ($finalState -ne 'COMPLETED|COMPLETED|1') { throw "Agent没有完成可靠停止:$finalState" }
$heartbeatBody = @{
sequence_no = 3; device_time = [DateTime]::UtcNow.ToString('o'); monotonic_ms = 52001
conversation_status = 'ACTIVE'; rtc = @{ joined = $true; uplink = $true; downlink = $true }
network = @{ type = 'wifi'; rssi = -50 }; firmware_version = '0.1.0-sim'; reported_config_revision = 1
recording_queue = @{ pending_segments = 0; failed_segments = 0 }; command_results = @()
rtc_credential_expires_at = [string]$created.rtc.expires_at
} | ConvertTo-Json -Compress -Depth 6
$heartbeat = ((SignedPost "/device/v2/conversations/$conversationId/heartbeat" $heartbeatBody '').Content | ConvertFrom-Json).data
if ($heartbeat.actions[0].type -ne 'stop') { throw '设备心跳没有收到停止动作。' }
$detail = (Invoke-RestMethod -Uri "$api/api/v1/conversations/$conversationId" -Headers @{ Authorization = "Bearer $accessToken" }).data
if ($detail.status -ne 'COMPLETED' -or $detail.stop_reason -ne 'consent_withdrawn:story_extraction') { throw '家庭通话详情没有展示授权撤回后的权威终态。' }
Write-Host '授权撤回停止烟测通过:SaaS事务意图、Agent可靠停止、设备stop动作和家庭通话终态一致。'
} finally {
if ($originalConsentVersion) {
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c `
"UPDATE consent_record SET withdrawn_at=NULL,version=$originalConsentVersion,updated_at=now() WHERE public_id='$consentId'::uuid;" | Out-Null
}
if ($conversationId -match '^[0-9a-fA-F-]{36}$') {
$cleanup = @"
BEGIN;
CREATE TEMP TABLE consent_stop_cleanup AS
SELECT id,public_id,device_id,context_snapshot_id,consent_snapshot_id FROM conversation WHERE public_id='$conversationId'::uuid;
DELETE FROM web_event WHERE resource_public_id IN(SELECT public_id FROM consent_stop_cleanup)
OR resource_public_id IN(SELECT public_id FROM device WHERE id IN(SELECT device_id FROM consent_stop_cleanup));
DELETE FROM conversation_summary WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
DELETE FROM conversation_turn WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
DELETE FROM provider_event WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
DELETE FROM conversation_state_history WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
DELETE FROM rtc_task WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
DELETE FROM outbox_event WHERE aggregate_id IN(SELECT id FROM consent_stop_cleanup);
DELETE FROM audit_event WHERE request_id='$withdrawRequestId' OR resource_id IN(SELECT id FROM consent_stop_cleanup) OR resource_id IN(SELECT public_id FROM consent_stop_cleanup);
DELETE FROM idempotency_record WHERE resource_id IN(SELECT id FROM consent_stop_cleanup);
DELETE FROM conversation WHERE id IN(SELECT id FROM consent_stop_cleanup);
DELETE FROM context_snapshot WHERE id IN(SELECT context_snapshot_id FROM consent_stop_cleanup);
DELETE FROM consent_snapshot WHERE id IN(SELECT consent_snapshot_id FROM consent_stop_cleanup);
COMMIT;
"@
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c $cleanup | Out-Null
docker compose exec -T redis redis-cli DEL "voice-platform:conversation:lease:$conversationId" "voice-platform:device:presence:$deviceId" | Out-Null
}
if ($withdrawalSnapshotId -match '^[0-9a-fA-F-]{36}$') {
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c `
"DELETE FROM consent_snapshot WHERE public_id='$withdrawalSnapshotId'::uuid AND NOT EXISTS(SELECT 1 FROM conversation WHERE consent_snapshot_id=consent_snapshot.id);" | Out-Null
}
Write-Host '已恢复合成授权并清理通话、快照、任务、审计和幂等记录。'
}