125 lines
11 KiB
PowerShell
125 lines
11 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|||
|
|
$PSNativeCommandUseErrorActionPreference = $true
|
||
|
|
|
||
|
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||
|
|
Set-Location -LiteralPath $projectRoot
|
||
|
|
$api = 'http://localhost:58080'
|
||
|
|
$deviceId = '10000000-0000-0000-0000-000000000511'
|
||
|
|
$deviceSecret = [Text.Encoding]::UTF8.GetBytes('local-simulated-device-secret-32-bytes-minimum')
|
||
|
|
$conversationId = $null
|
||
|
|
$createKey = 'hardware-create-' + [Guid]::NewGuid().ToString('N')
|
||
|
|
$deviceInternalId = (docker compose exec -T postgres psql -U voice -d voice_platform -t -A -c "SELECT id FROM device WHERE public_id='$deviceId'::uuid" | Out-String).Trim()
|
||
|
|
$existingHardwareBinding = (docker compose exec -T postgres psql -U voice -d voice_platform -t -A -c "SELECT count(*) FROM hardware_agent_device_binding WHERE device_id='$deviceInternalId'::uuid" | Out-String).Trim()
|
||
|
|
if ($existingHardwareBinding -ne '0') { throw 'smoke device already has a hardware binding; refusing to overwrite it' }
|
||
|
|
"UPDATE device_twin SET desired_config=desired_config||'{`"voice_runtime`":`"volc_hardware_agent`",`"hardware_agent_transport`":`"high_quality`",`"minimum_sdk_version`":`"1.0.0`"}'::jsonb WHERE device_id='$deviceInternalId'::uuid; INSERT INTO hardware_agent_device_binding(tenant_id,device_id,bot_id,transport,minimum_sdk_version,status) SELECT tenant_id,id,'local-hardware-agent-bot','high_quality','1.0.0','ACTIVE' FROM device WHERE id='$deviceInternalId'::uuid;" | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q | Out-Null
|
||
|
|
|
||
|
|
function Invoke-SignedPost([string]$Path, [object]$Payload, [string]$IdempotencyKey = '') {
|
||
|
|
$body = $Payload | ConvertTo-Json -Depth 12 -Compress
|
||
|
|
$bodyBytes = [Text.Encoding]::UTF8.GetBytes($body)
|
||
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
||
|
|
try { $digest = ([BitConverter]::ToString($sha.ComputeHash($bodyBytes))).Replace('-','').ToLowerInvariant() } finally { $sha.Dispose() }
|
||
|
|
$timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||
|
|
$requestNonce = [Guid]::NewGuid().ToString('N')
|
||
|
|
$canonical = "POST`n$Path`n$timestamp`n$requestNonce`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'=$requestNonce
|
||
|
|
'X-Device-Content-SHA256'=$digest; 'X-Device-Signature'=$signature
|
||
|
|
}
|
||
|
|
if ($IdempotencyKey) { $headers['Idempotency-Key'] = $IdempotencyKey }
|
||
|
|
return (Invoke-RestMethod -Method Post -Uri ($api+$Path) -ContentType 'application/json' -Headers $headers -Body $body).data
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$baseCapabilities = @{hardware_agent=$true;sdk_version='1.2.0';transports=@('high_quality','low_load');input_audio_format='pcm_s16le_16000_mono';output_audio_format='pcm_s16le_24000_mono';server_event_callback=$true;transcript_upload=$true;dynamic_session_config=$true;credential_rotation=$true;near_pcm_recording=$false;far_pcm_recording=$false}
|
||
|
|
$oldCapabilities = $baseCapabilities.Clone()
|
||
|
|
$oldCapabilities.sdk_version = '0.9.0'
|
||
|
|
try {
|
||
|
|
Invoke-SignedPost '/device/v3/agent-sessions' @{
|
||
|
|
device_time=[DateTime]::UtcNow.ToString('o');monotonic_ms=1;config_revision=2;capabilities=$oldCapabilities
|
||
|
|
network=@{type='local-smoke'};requested_goal='open_conversation'
|
||
|
|
} ('hardware-old-sdk-'+[Guid]::NewGuid().ToString('N')) | Out-Null
|
||
|
|
throw 'old hardware SDK unexpectedly passed the admission gate'
|
||
|
|
} catch {
|
||
|
|
if ($_.Exception.Response.StatusCode.value__ -ne 426) { throw }
|
||
|
|
}
|
||
|
|
|
||
|
|
$created = Invoke-SignedPost '/device/v3/agent-sessions' @{
|
||
|
|
device_time=[DateTime]::UtcNow.ToString('o'); monotonic_ms=1; config_revision=2
|
||
|
|
capabilities=$baseCapabilities
|
||
|
|
network=@{type='local-smoke'}; requested_goal='open_conversation'
|
||
|
|
} $createKey
|
||
|
|
$conversationId = $created.conversation_id
|
||
|
|
if ($created.status -ne 'SESSION_READY' -or $created.runtime_type -ne 'volc_hardware_agent') { throw 'hardware prefetch state is invalid' }
|
||
|
|
if (-not $created.runtime.session_nonce -or -not $created.runtime.config_digest -or -not $created.session_config.authorized_context -or -not $created.session_config.tool_url -or -not $created.session_config.tool_scope_token) { throw 'hardware frozen configuration or scoped memory tool is incomplete' }
|
||
|
|
if ($created.privacy.retention_mode -eq 'transcript_and_audio' -or $created.privacy.record_near -or $created.privacy.record_far -or $created.privacy.upload_allowed) { throw 'hardware privacy was not downgraded for missing dual PCM recording capability' }
|
||
|
|
|
||
|
|
$common = @{ provider_session_ref=$created.runtime.provider_session_ref; session_nonce=$created.runtime.session_nonce }
|
||
|
|
try {
|
||
|
|
Invoke-SignedPost "/device/v3/agent-sessions/$conversationId/events" ($common + @{
|
||
|
|
event_id='bad-start-'+[Guid]::NewGuid().ToString('N');sequence_no=1;event_type='session_started';occurred_at=[DateTime]::UtcNow.ToString('o');provider_connection_ref='volc-smoke-session';bot_id=$created.runtime.bot_id;transport=$created.runtime.transport;config_digest=$created.runtime.config_digest;dynamic_config_applied=$false
|
||
|
|
}) | Out-Null
|
||
|
|
throw 'session unexpectedly activated before dynamic configuration was applied'
|
||
|
|
} catch {
|
||
|
|
if ($_.Exception.Response.StatusCode.value__ -ne 400) { throw }
|
||
|
|
}
|
||
|
|
$started = Invoke-SignedPost "/device/v3/agent-sessions/$conversationId/events" ($common + @{
|
||
|
|
event_id='start-'+[Guid]::NewGuid().ToString('N');sequence_no=1;event_type='session_started';occurred_at=[DateTime]::UtcNow.ToString('o');provider_connection_ref='volc-smoke-session';bot_id=$created.runtime.bot_id;transport=$created.runtime.transport;config_digest=$created.runtime.config_digest;dynamic_config_applied=$true
|
||
|
|
})
|
||
|
|
if ($started.status -ne 'ACTIVE' -or $started.action -ne 'continue') { throw 'hardware start event did not activate conversation' }
|
||
|
|
|
||
|
|
$toolResult = Invoke-RestMethod -Method Post -Uri ($api+'/internal/v1/tools/memory-search') -ContentType 'application/json' -Headers @{Authorization='Bearer '+$created.session_config.tool_scope_token} -Body '{"query":"today preference","limit":1}'
|
||
|
|
if ($null -eq $toolResult.data.items) { throw 'hardware scoped memory tool did not accept the active hardware session' }
|
||
|
|
|
||
|
|
$now = [DateTime]::UtcNow
|
||
|
|
$turns = Invoke-SignedPost "/device/v3/agent-sessions/$conversationId/turns:batch" @{
|
||
|
|
turns=@(@{turn_key='hardware-smoke-1';sequence_no=1;speaker='subject';start_at=$now.ToString('o');end_at=$now.AddSeconds(1).ToString('o');final_text='I feel good today';interrupted=$false})
|
||
|
|
} ('hardware-turns-'+[Guid]::NewGuid().ToString('N'))
|
||
|
|
if ($turns.accepted_count -ne 1) { throw 'hardware transcript was not stored' }
|
||
|
|
|
||
|
|
$stop = Invoke-SignedPost "/device/v2/conversations/$conversationId/stop" @{
|
||
|
|
reason='hardware_smoke_complete';device_time=[DateTime]::UtcNow.ToString('o');last_heartbeat_sequence=0;rtc_left=$true;recording=@{near_segments=0;far_segments=0}
|
||
|
|
} ('hardware-stop-'+[Guid]::NewGuid().ToString('N'))
|
||
|
|
if ($stop.status -ne 'STOPPING') { throw 'platform stop did not move hardware session to STOPPING' }
|
||
|
|
|
||
|
|
$updated = Invoke-SignedPost "/device/v3/agent-sessions/$conversationId/events" ($common + @{
|
||
|
|
event_id='updated-'+[Guid]::NewGuid().ToString('N');sequence_no=2;event_type='session_updated';occurred_at=[DateTime]::UtcNow.ToString('o')
|
||
|
|
})
|
||
|
|
if ($updated.status -ne 'STOPPING' -or $updated.action -ne 'stop_session') { throw 'hardware liveness response did not deliver stop_session' }
|
||
|
|
|
||
|
|
$ended = Invoke-SignedPost "/device/v3/agent-sessions/$conversationId/events" ($common + @{
|
||
|
|
event_id='ended-'+[Guid]::NewGuid().ToString('N');sequence_no=3;event_type='session_ended';occurred_at=[DateTime]::UtcNow.ToString('o')
|
||
|
|
})
|
||
|
|
if ($ended.status -ne 'COMPLETED' -or $ended.action -ne 'stop_session') { throw 'hardware end event did not close conversation' }
|
||
|
|
|
||
|
|
$databaseState = (docker compose exec -T postgres psql -U voice -d voice_platform -t -A -F '|' -c "SELECT c.runtime_type||'|'||c.status||'|'||hs.status||'|'||hs.sdk_version||'|'||(hs.dynamic_config_applied_at IS NOT NULL)::text||'|'||count(DISTINCT ct.id)||'|'||(SELECT count(*) FROM web_event we WHERE we.resource_public_id=c.public_id AND we.event_type IN('conversation.started','conversation.completed')) FROM conversation c JOIN hardware_agent_session hs ON hs.conversation_id=c.id LEFT JOIN conversation_turn ct ON ct.conversation_id=c.id WHERE c.public_id='$conversationId'::uuid GROUP BY c.runtime_type,c.status,hs.status,hs.sdk_version,hs.dynamic_config_applied_at,c.public_id" | Out-String).Trim()
|
||
|
|
if ($databaseState -ne 'volc_hardware_agent|COMPLETED|COMPLETED|1.2.0|true|1|2') { throw "hardware database/SaaS event state is invalid: $databaseState" }
|
||
|
|
Write-Host "hardware agent smoke PASS: $conversationId"
|
||
|
|
} finally {
|
||
|
|
if ($conversationId -match '^[0-9a-fA-F-]{36}$') {
|
||
|
|
@"
|
||
|
|
BEGIN;
|
||
|
|
CREATE TEMP TABLE hc AS SELECT id,context_snapshot_id,consent_snapshot_id FROM conversation WHERE public_id='$conversationId'::uuid;
|
||
|
|
DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM web_event WHERE resource_public_id='$conversationId'::uuid;
|
||
|
|
DELETE FROM idempotency_record WHERE resource_id IN(SELECT id FROM hc) OR idempotency_key='$createKey';
|
||
|
|
DELETE FROM conversation_turn WHERE conversation_id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM conversation_memory_use WHERE conversation_id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM memory_tool_call WHERE conversation_id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM hardware_agent_event WHERE conversation_id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM hardware_agent_session WHERE conversation_id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM conversation_usage_budget WHERE conversation_id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM outbox_event WHERE aggregate_id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM conversation_state_history WHERE conversation_id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM conversation WHERE id IN(SELECT id FROM hc);
|
||
|
|
DELETE FROM context_snapshot WHERE id IN(SELECT context_snapshot_id FROM hc);
|
||
|
|
DELETE FROM consent_snapshot WHERE id IN(SELECT consent_snapshot_id FROM hc);
|
||
|
|
COMMIT;
|
||
|
|
"@ | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q | Out-Null
|
||
|
|
}
|
||
|
|
if ($deviceInternalId -match '^[0-9a-fA-F-]{36}$') {
|
||
|
|
"DELETE FROM hardware_agent_device_binding WHERE device_id='$deviceInternalId'::uuid; UPDATE device_twin SET desired_config=desired_config-'voice_runtime'-'hardware_agent_transport'-'minimum_sdk_version' WHERE device_id='$deviceInternalId'::uuid;" | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q | Out-Null
|
||
|
|
}
|
||
|
|
}
|