2026-08-26 06:41:07 +08:00
$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'
$tenantPublicId = '10000000-0000-0000-0000-000000000010'
$deviceId = '10000000-0000-0000-0000-000000000511'
$deviceSecret =[ Text.Encoding ]:: UTF8 . GetBytes ( 'local-simulated-device-secret-32-bytes-minimum' )
$configIds = @ (); $resourceIds = @ (); $conversationId = $null ; $groupId = $null
$key1 = 'ops-config-' +[ Guid ]:: NewGuid (). ToString ( 'N' ); $key2 = 'ops-config-' +[ Guid ]:: NewGuid (). ToString ( 'N' ); $rollbackKey = 'ops-rollback-' +[ Guid ]:: NewGuid (). ToString ( 'N' )
2026-08-29 21:56:51 +08:00
function Token([string]$name){$password=if($name-like'operator*'){'OpsLocalOnly123!'}else{'LocalOnly123!'};(Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @ { grant_type = 'password' ; client_id = 'voice-platform-web' ; username = $name ; password = $password ; scope = 'openid' }). access_token }
2026-08-26 06:41:07 +08:00
function Expected([scriptblock]$call,[int]$status){try{&$call|Out-Null;throw "Expected HTTP $status " } catch { $actual =[ int ] $_ . Exception . Response . StatusCode ; if ( $actual -ne $status ){ throw }}}
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()};Invoke-WebRequest -UseBasicParsing -Method Post -Uri ( $api + $path ) -ContentType 'application/json' -Body $body -Headers @ { 'X-Device-Id' = $deviceId ; 'X-Device-Timestamp' = $timestamp ; 'X-Device-Nonce' = $nonce ; 'X-Device-Content-SHA256' = $digest ; 'X-Device-Signature' = $signature ; 'Idempotency-Key' = $key }}
function RolloutBody([string]$reason,[string]$scope,[string]$scopeId=''){@{reason=$reason;rollout=@{scope_type=$scope;rollout_percent=100;max_concurrent_calls=100}}|ForEach-Object{if($scopeId){$_.rollout.scope_id=$scopeId};$_|ConvertTo-Json -Compress -Depth 6 }}
function AddEvaluation([string]$configId,[string]$suite,[string]$token){$body=@{suite_version=$suite;result='PASSED';metrics=@{success_rate=1;sample_count=12};report_ref=("report://local/"+$suite)}|ConvertTo-Json -Compress -Depth 6 ;( Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/config-versions/ $configId /evaluations" -Headers @ { Authorization = "Bearer $token " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' =( 'ops-eval-' +[ Guid ]:: NewGuid (). ToString ( 'N' ))} -ContentType 'application/json' -Body $body ). Content | ConvertFrom-Json }
function AddResource([string]$type,[string]$name,[string]$display,[string]$token){$body=@{resource_type=$type;reference_name=$name;display_name=$display;verified_at=[DateTime]::UtcNow.ToString('o')}|ConvertTo-Json -Compress ;( Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/resource-references" -Headers @ { Authorization = "Bearer $token " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' =( 'ops-resource-' +[ Guid ]:: NewGuid (). ToString ( 'N' ))} -ContentType 'application/json' -Body $body ). Content | ConvertFrom-Json }
try {
2026-08-29 21:56:51 +08:00
$owner = Token 'operator' ; $editor = Token 'operator-approver' ; $outsider = Token 'outsider'
2026-08-26 06:41:07 +08:00
$ownerMe =(( Invoke-WebRequest -UseBasicParsing -Uri " $api /api/v1/me" -Headers @ { Authorization = "Bearer $owner " }). Content | ConvertFrom-Json ). data
$editorMe =(( Invoke-WebRequest -UseBasicParsing -Uri " $api /api/v1/me" -Headers @ { Authorization = "Bearer $editor " }). Content | ConvertFrom-Json ). data
2026-08-29 21:56:51 +08:00
if ( $ownerMe . accounts [ 0 ]. account_kind-ne 'operator' -or $ownerMe . accounts [ 0 ]. platform_role-ne 'ops_admin' -or $editorMe . accounts [ 0 ]. account_kind-ne 'operator' -or $editorMe . accounts [ 0 ]. platform_role-ne 'ops_approver' ){ throw 'Dedicated operations accounts were not exposed by /me' }
2026-08-26 06:41:07 +08:00
Expected { Invoke-WebRequest -UseBasicParsing -Uri " $api /api/v1/ops/config-versions" -Headers @ { Authorization = "Bearer $outsider " ; 'X-Tenant-Id' = '20000000-0000-0000-0000-000000000010' }} 403
$groupBody = @ { name =( '本地灰度设备组-' +[ Guid ]:: NewGuid (). ToString ( 'N' ). Substring ( 0 , 8 )); selector = @ { source = 'smoke-test' }; device_ids = @ ( $deviceId )}| ConvertTo-Json -Compress -Depth 5
$groupResponse = Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/device-groups" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' =( 'ops-group-' +[ Guid ]:: NewGuid (). ToString ( 'N' ))} -ContentType 'application/json' -Body $groupBody
$groupId =[ string ](( $groupResponse . Content | ConvertFrom-Json ). data . id )
if ( -not $groupId ){ throw 'Device rollout group was not created' }
$invalid = @ { config_key = 'default-agent-routing' ; display_name = '默认语音路由' ; config_kind = 'ROUTING' ; payload = @ { default_engine_mode = 's2s_hybrid' ; routes = @ (); api_secret = 'must-not-pass' }; secret_refs = @ {}; change_summary = '非法明文密钥验证' }| ConvertTo-Json -Compress -Depth 8
Expected { Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/config-versions" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' =( 'invalid-' +[ Guid ]:: NewGuid (). ToString ( 'N' ))} -ContentType 'application/json' -Body $invalid } 400
$v1Body = @ { config_key = 'default-agent-routing' ; display_name = '默认语音路由' ; config_kind = 'ROUTING' ; payload = @ { default_engine_mode = 's2s_hybrid' ; routes = @ ( @ { goal = 'open_conversation' ; engine_mode = 's2s_hybrid' })}; secret_refs = @ {}; change_summary = '建立默认混合语音路由' }| ConvertTo-Json -Compress -Depth 8
$v1Response = Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/config-versions" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' = $key1 } -ContentType 'application/json' -Body $v1Body
$v1 =( $v1Response . Content | ConvertFrom-Json ). data ; $configIds +=[ string ] $v1 . id
$replay = Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/config-versions" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' = $key1 } -ContentType 'application/json' -Body $v1Body
if ( $replay . StatusCode-ne200 -or [ string ] $replay . Headers . 'Idempotency-Replayed' -ne 'true' -or (( $replay . Content | ConvertFrom-Json ). data . id-ne $v1 . id )){ throw 'Config draft idempotency replay failed' }
$v1Submitted =(( Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/config-versions/ $( $v1 . id ) /submit" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v1"' }). Content | ConvertFrom-Json ). data
$tenantPublish = RolloutBody '审批通过第一版' 'TENANT'
Expected { Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/config-versions/ $( $v1 . id ) /publish" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v2"' } -ContentType 'application/json' -Body ( RolloutBody '禁止自己审批' 'TENANT' )} 409
Expected { Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/config-versions/ $( $v1 . id ) /publish" -Headers @ { Authorization = "Bearer $editor " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v2"' } -ContentType 'application/json' -Body $tenantPublish } 409
AddEvaluation ([ string ] $v1 . id ) 'routing-regression-v1' $owner | Out-Null
$evidence =(( Invoke-WebRequest -UseBasicParsing -Uri " $api /api/v1/ops/config-versions/ $( $v1 . id ) /evaluations" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId }). Content | ConvertFrom-Json ). data . items
if ( @ ( $evidence ). Count-ne1 -or $evidence [ 0 ]. result-ne 'PASSED' ){ throw 'Offline evaluation evidence was not queryable' }
$v1Published =(( Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/config-versions/ $( $v1 . id ) /publish" -Headers @ { Authorization = "Bearer $editor " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v2"' } -ContentType 'application/json' -Body $tenantPublish ). Content | ConvertFrom-Json ). data
if ( $v1Published . status-ne 'PUBLISHED' -or $v1Published . validation_result . valid-ne $true ){ throw 'First config was not published with validation evidence' }
$v2Body = @ { config_key = 'default-agent-routing' ; display_name = '默认语音路由' ; config_kind = 'ROUTING' ; payload = @ { default_engine_mode = 'cascade' ; routes = @ ( @ { goal = 'open_conversation' ; engine_mode = 'cascade' })}; secret_refs = @ {}; change_summary = '切换到级联链路进行小范围验证' }| ConvertTo-Json -Compress -Depth 8
$v2 =(( Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/config-versions" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' = $key2 } -ContentType 'application/json' -Body $v2Body ). Content | ConvertFrom-Json ). data ; $configIds +=[ string ] $v2 . id
Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/config-versions/ $( $v2 . id ) /submit" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v1"' }| Out-Null
AddEvaluation ([ string ] $v2 . id ) 'routing-regression-v2' $owner | Out-Null
$groupPublish = RolloutBody '审批通过第二版并投放设备组' 'DEVICE_GROUP' $groupId
$v2Published =(( Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/config-versions/ $( $v2 . id ) /publish" -Headers @ { Authorization = "Bearer $editor " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v2"' } -ContentType 'application/json' -Body $groupPublish ). Content | ConvertFrom-Json ). data
$history =(( Invoke-WebRequest -UseBasicParsing -Uri " $api /api/v1/ops/config-versions?config_key=default-agent-routing" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId }). Content | ConvertFrom-Json ). data . items
$published = @ ( $history | Where-Object { $_ . status -eq 'PUBLISHED' }); $first = @ ( $history | Where-Object { $_ . id -eq $v1 . id })
if ( $published . Count-ne2 -or $first . Count-ne1 -or $first [ 0 ]. status-ne 'PUBLISHED' ){ throw "Independent tenant and device-group rollouts were not retained: $( $history | ForEach-Object { " $( $_ . version_no ) : $( $_ . status ) " }) " }
$rollbackBody = RolloutBody '级联验证异常,回滚设备组到混合语音路由' 'DEVICE_GROUP' $groupId
$rollbackResponse = Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/config-versions/ $( $v1 . id ) /rollback" -Headers @ { Authorization = "Bearer $editor " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' = $rollbackKey } -ContentType 'application/json' -Body $rollbackBody
$rollback =( $rollbackResponse . Content | ConvertFrom-Json ). data ; $configIds +=[ string ] $rollback . id
$rollbackReplay = Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/config-versions/ $( $v1 . id ) /rollback" -Headers @ { Authorization = "Bearer $editor " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' = $rollbackKey } -ContentType 'application/json' -Body $rollbackBody
if ( $rollback . version_no-ne3 -or $rollback . payload . default_engine_mode-ne 's2s_hybrid' -or ( $rollbackReplay . Content | ConvertFrom-Json ). data . id-ne $rollback . id-or [ string ] $rollbackReplay . Headers . 'Idempotency-Replayed' -ne 'true' ){ throw 'Immutable rollback version or replay failed' }
$afterRollback =(( Invoke-WebRequest -UseBasicParsing -Uri " $api /api/v1/ops/config-versions?config_key=default-agent-routing" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId }). Content | ConvertFrom-Json ). data . items
$retiredV2 = @ ( $afterRollback | Where-Object { $_ . id-eq $v2 . id }); if ( $retiredV2 . Count-ne1 -or $retiredV2 [ 0 ]. status-ne 'RETIRED' -or $rollback . supersedes_id-ne $v2 . id ){ throw 'Device-group rollback did not retire and link the superseded rollout version' }
$resourceSuffix =[ Guid ]:: NewGuid (). ToString ( 'N' ); $credentialRef = "kms://smoke/credential/ $resourceSuffix " ; $modelRef = "model://smoke/approved/ $resourceSuffix "
$modelBody = @ { config_key =( "smoke-model-" + $resourceSuffix . Substring ( 0 , 10 )); display_name = '模型资源门禁验证' ; config_kind = 'MODEL' ; payload = @ { provider = 'local-smoke' ; model_id = $modelRef }; secret_refs = @ { credential_ref = $credentialRef }; change_summary = '验证模型与凭据资源类型及时效门禁' }| ConvertTo-Json -Compress -Depth 8
$model =(( Invoke-WebRequest -UseBasicParsing -Method Post -Uri " $api /api/v1/ops/config-versions" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'Idempotency-Key' =( 'ops-model-' +[ Guid ]:: NewGuid (). ToString ( 'N' ))} -ContentType 'application/json' -Body $modelBody ). Content | ConvertFrom-Json ). data ; $configIds +=[ string ] $model . id
Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/config-versions/ $( $model . id ) /submit" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v1"' }| Out-Null
AddEvaluation ([ string ] $model . id ) 'model-resource-gate-v1' $owner | Out-Null
$modelPublish = RolloutBody '模型资源门禁验证通过' 'TENANT'
Expected { Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/config-versions/ $( $model . id ) /publish" -Headers @ { Authorization = "Bearer $editor " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v2"' } -ContentType 'application/json' -Body $modelPublish } 409
$credential =( AddResource 'PROVIDER_CREDENTIAL' $credentialRef '本地凭据引用' $owner ). data ; $resourceIds +=[ string ] $credential . id
$modelResource =( AddResource 'MODEL_RESOURCE' $modelRef '本地已核验模型' $owner ). data ; $resourceIds +=[ string ] $modelResource . id
$modelPublished =(( Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/config-versions/ $( $model . id ) /publish" -Headers @ { Authorization = "Bearer $editor " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v2"' } -ContentType 'application/json' -Body $modelPublish ). Content | ConvertFrom-Json ). data
if ( $modelPublished . status-ne 'PUBLISHED' ){ throw 'Verified credential and model references did not release the publish gate' }
$activeRollouts = @ ((( Invoke-WebRequest -UseBasicParsing -Uri " $api /api/v1/ops/config-rollouts" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId }). Content | ConvertFrom-Json ). data . items | Where-Object { $_ . status-eq 'ACTIVE' -and $_ . config_version_id-in $configIds })
if ( $activeRollouts . Count-ne3 ){ throw "Active tenant, device-group and model rollout ledger mismatch: $( $activeRollouts . Count ) " }
$revoked =(( Invoke-WebRequest -UseBasicParsing -Method Patch -Uri " $api /api/v1/ops/resource-references/ $( $modelResource . id ) /revoke" -Headers @ { Authorization = "Bearer $owner " ; 'X-Tenant-Id' = $tenantPublicId ; 'If-Match' = '"v1"' } -ContentType 'application/json' -Body ( @ { reason = '验证资源撤销生命周期' }| ConvertTo-Json -Compress )). Content | ConvertFrom-Json ). data
if ( $revoked . status-ne 'REVOKED' -or $revoked . row_version-ne2 ){ throw 'Resource reference revocation did not preserve optimistic concurrency' }
$createBody = @ { device_time =[ DateTime ]:: UtcNow . ToString ( 'o' ); monotonic_ms = 98765 ; capabilities = @ { rtc = $true ; audio_tracks = @ ( 'near' , 'far' ); upload_resume = $true }; network = @ { type = 'wifi' }; requested_goal = 'open_conversation' }| ConvertTo-Json -Compress -Depth 8
$created =( SignedPost '/device/v2/conversations' $createBody ( 'ops-runtime-' +[ Guid ]:: NewGuid (). ToString ( 'N' ))). Content | ConvertFrom-Json
$conversationId =[ string ] $created . data . conversation_id
$runtime =( docker compose exec -T postgres psql -U voice -d voice_platform -t -A -c "SELECT c.engine_mode||'|'||xs.prompt_bundle_version FROM conversation c JOIN context_snapshot xs ON xs.id=c.context_snapshot_id WHERE c.public_id=' $conversationId '::uuid" | Out-String ). Trim ()
if ( $runtime -ne ( "s2s_hybrid|cfg:" + $rollback . id )){ throw "New conversation did not freeze rolled-back config: $runtime " }
$auditCount =[ int ](( docker compose exec -T postgres psql -U voice -d voice_platform -t -A -c "SELECT count(*) FROM audit_event WHERE action LIKE 'ops.config.%' AND resource_id IN(SELECT id FROM runtime_config_version WHERE public_id IN(' $( $configIds [ 0 ]) '::uuid,' $( $configIds [ 1 ]) '::uuid,' $( $configIds [ 2 ]) '::uuid,' $( $configIds [ 3 ]) '::uuid))" | Out-String ). Trim ())
if ( $auditCount -ne10 ){ throw "Unexpected operations config-version audit count: $auditCount " }
Write-Host "Operations config smoke passed: role isolation, evaluation/resource gates, rollout ledger and revocation, device-group precedence, immutable rollback and new-call version freeze: $( $rollback . id ) "
} finally {
if ( $conversationId -match '^[0-9a-fA-F-]{36}$' ){
$cleanupConversation = @"
BEGIN;
CREATE TEMP TABLE ops_conv AS SELECT id,context_snapshot_id,consent_snapshot_id FROM conversation WHERE public_id=' $conversationId '::uuid;
DELETE FROM conversation_state_history WHERE conversation_id IN(SELECT id FROM ops_conv);
DELETE FROM rtc_task WHERE conversation_id IN(SELECT id FROM ops_conv);
DELETE FROM outbox_event WHERE aggregate_id IN(SELECT id FROM ops_conv);
DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM ops_conv);
DELETE FROM idempotency_record WHERE resource_id IN(SELECT id FROM ops_conv);
DELETE FROM conversation WHERE id IN(SELECT id FROM ops_conv);
DELETE FROM context_snapshot WHERE id IN(SELECT context_snapshot_id FROM ops_conv);
DELETE FROM consent_snapshot WHERE id IN(SELECT consent_snapshot_id FROM ops_conv);
COMMIT;
"@
$cleanupConversation | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP = 1 -q | Out-Null
}
if ( $configIds . Count ){ $ids =( $configIds | ForEach-Object { "' $_ '::uuid" }) -join ',' ; $cleanup = "BEGIN; DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM ops_config_evaluation WHERE config_version_id IN(SELECT id FROM runtime_config_version WHERE public_id IN( $ids ))); DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM runtime_config_version WHERE public_id IN( $ids )); DELETE FROM idempotency_record WHERE scope IN('POST /api/v1/ops/config-versions','POST /api/v1/ops/config-versions/rollback','POST /api/v1/ops/config-versions/evaluations') AND (resource_id IN(SELECT id FROM runtime_config_version WHERE public_id IN( $ids )) OR resource_id IN(SELECT id FROM ops_config_evaluation WHERE config_version_id IN(SELECT id FROM runtime_config_version WHERE public_id IN( $ids )))); DELETE FROM runtime_config_rollout WHERE config_version_id IN(SELECT id FROM runtime_config_version WHERE public_id IN( $ids )); UPDATE runtime_config_version SET supersedes_id=NULL,rollback_of_id=NULL WHERE public_id IN( $ids ); DELETE FROM runtime_config_version WHERE public_id IN( $ids ); COMMIT;" ; $cleanup | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP = 1 -q | Out-Null }
if ( $resourceIds . Count ){ $ids =( $resourceIds | ForEach-Object { "' $_ '::uuid" }) -join ',' ; $cleanupResources = "BEGIN; DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM ops_resource_reference WHERE public_id IN( $ids )); DELETE FROM idempotency_record WHERE scope='POST /api/v1/ops/resource-references' AND resource_id IN(SELECT id FROM ops_resource_reference WHERE public_id IN( $ids )); DELETE FROM ops_resource_reference WHERE public_id IN( $ids ); COMMIT;" ; $cleanupResources | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP = 1 -q | Out-Null }
if ( $groupId -match '^[0-9a-fA-F-]{36}$' ){ $cleanupGroup = "BEGIN; DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM ops_device_group WHERE public_id=' $groupId '::uuid); DELETE FROM idempotency_record WHERE scope='POST /api/v1/ops/device-groups' AND resource_id IN(SELECT id FROM ops_device_group WHERE public_id=' $groupId '::uuid); DELETE FROM ops_device_group WHERE public_id=' $groupId '::uuid; COMMIT;" ; $cleanupGroup | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP = 1 -q | Out-Null }
}