param( [ValidateSet('llm', 's2s')] [string]$ModelMode = 'llm', [string]$ExecutablePath = '' ) $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true $simulatorRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path $projectRoot = (Resolve-Path -LiteralPath (Join-Path $simulatorRoot '..')).Path $devicePublicID = '10000000-0000-0000-0000-000000000511' $platformSecret = 'local-simulated-device-secret-32-bytes-minimum' $volcSecret = 'mock-volc-device-secret-at-least-32-characters' $mockPort = 19090 $runID = [Guid]::NewGuid().ToString('N') $resultPath = Join-Path $env:TEMP "hardware-agent-e2e-$runID.json" $mockOutput = Join-Path $env:TEMP "hardware-agent-gateway-$runID.log" $mockError = Join-Path $env:TEMP "hardware-agent-gateway-$runID.err.log" $mockProcess = $null $conversationID = $null $deviceInternalID = $null Set-Location -LiteralPath $projectRoot try { $ready = Invoke-WebRequest -UseBasicParsing -Uri 'http://localhost:58080/health/ready' -TimeoutSec 10 if ($ready.StatusCode -ne 200) { throw 'Local API is not ready' } $deviceInternalID = (docker compose exec -T postgres psql -U voice -d voice_platform -t -A -c "SELECT id FROM device WHERE public_id='$devicePublicID'::uuid" | Out-String).Trim() if ($deviceInternalID -notmatch '^[0-9a-fA-F-]{36}$') { throw 'Local simulator device is missing' } $bindingCount = (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 ($bindingCount -ne '0') { throw 'Simulator 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`":`"low_load`",`"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,'mock-windows-hardware-bot','low_load','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 $mockScript = Join-Path $simulatorRoot 'test\mock-hardware-gateway.cjs' $env:MOCK_HARDWARE_GATEWAY_PORT = [string]$mockPort $env:MOCK_VOLC_DEVICE_SECRET = $volcSecret $env:MOCK_REQUEST_PLATFORM_STOP = '1' $env:MOCK_PLATFORM_API_URL = 'http://localhost:58080' $env:MOCK_PLATFORM_DEVICE_ID = $devicePublicID $env:MOCK_PLATFORM_DEVICE_SECRET = $platformSecret $mockProcess = Start-Process -FilePath 'node' -ArgumentList @($mockScript) -PassThru -WindowStyle Hidden -RedirectStandardOutput $mockOutput -RedirectStandardError $mockError $deadline = [DateTime]::UtcNow.AddSeconds(10) do { Start-Sleep -Milliseconds 150 $listening = Test-NetConnection 127.0.0.1 -Port $mockPort -InformationLevel Quiet -WarningAction SilentlyContinue if ($mockProcess.HasExited) { throw "Mock gateway exited: $(Get-Content -LiteralPath $mockError -Raw -ErrorAction SilentlyContinue)" } } until ($listening -or [DateTime]::UtcNow -gt $deadline) if (-not $listening) { throw 'Mock hardware gateway did not start' } $env:SIMULATOR_AUTO_TEST = '1' $env:SIMULATOR_AUTO_WAIT_FOR_PLATFORM_STOP = '1' $env:SIMULATOR_AUTO_RESULT_PATH = $resultPath $env:SIMULATOR_DEFAULT_API_URL = 'http://localhost:58080' $env:SIMULATOR_DEFAULT_DEVICE_ID = $devicePublicID $env:SIMULATOR_DEFAULT_DEVICE_SECRET = $platformSecret $env:SIMULATOR_VOLC_INSTANCE_ID = 'mock-instance' $env:SIMULATOR_VOLC_PRODUCT_KEY = 'mock-product' $env:SIMULATOR_VOLC_DEVICE_NAME = 'windows-e2e-device' $env:SIMULATOR_VOLC_HARDWARE_ID = 'windows-e2e-hardware' $env:SIMULATOR_VOLC_DEVICE_SECRET = $volcSecret $env:SIMULATOR_VOLC_MODEL_MODE = $ModelMode $env:SIMULATOR_HARDWARE_GATEWAY_URL = "ws://127.0.0.1:$mockPort/v1/realtime" Set-Location -LiteralPath $simulatorRoot if ($ExecutablePath) { $resolvedExecutable = (Resolve-Path -LiteralPath $ExecutablePath).Path $electron = Start-Process -FilePath $resolvedExecutable -WorkingDirectory (Split-Path -Parent $resolvedExecutable) -PassThru -Wait } else { $electron = Start-Process -FilePath (Join-Path $simulatorRoot 'node_modules\electron\dist\electron.exe') -ArgumentList @('.') -WorkingDirectory $simulatorRoot -PassThru -Wait } if ($electron.ExitCode -ne 0) { throw "Electron E2E process failed with exit code $($electron.ExitCode)" } if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) { throw 'Electron E2E result file is missing' } $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json if ($result.result -ne 'PASS' -or $result.phase -ne '已完成') { throw "Electron E2E failed: $($result | ConvertTo-Json -Depth 8 -Compress)" } if ($result.model_mode -ne $ModelMode) { throw "Electron E2E used model mode '$($result.model_mode)' instead of '$ModelMode'" } $conversationID = [string]$result.conversation_id if ($conversationID -notmatch '^[0-9a-fA-F-]{36}$') { throw 'Electron E2E did not return a conversation ID' } $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.1.0-windows-standard-protocol|true|2|2') { throw "Unexpected platform state: $databaseState" } $gatewayEvents = Get-Content -LiteralPath $mockOutput -Raw if ($gatewayEvents -notmatch 'dynamic_config_applied' -or $gatewayEvents -notmatch 'round_completed' -or $gatewayEvents -notmatch 'platform_stop_requested') { throw "Mock gateway did not observe config, media, and platform-stop flow: $gatewayEvents" } [ordered]@{ result = 'PASS' conversation_id = $conversationID platform_state = $databaseState transcript_turns = $result.turns.Count checks_passed = @($result.checks | Where-Object status -eq 'passed').Count model_mode = $result.model_mode mock_gateway_log = $mockOutput } | ConvertTo-Json -Compress } finally { Set-Location -LiteralPath $projectRoot if ($mockProcess -and -not $mockProcess.HasExited) { Stop-Process -Id $mockProcess.Id -Force -ErrorAction SilentlyContinue } 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); 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 } @('MOCK_HARDWARE_GATEWAY_PORT','MOCK_VOLC_DEVICE_SECRET','MOCK_REQUEST_PLATFORM_STOP','MOCK_PLATFORM_API_URL','MOCK_PLATFORM_DEVICE_ID','MOCK_PLATFORM_DEVICE_SECRET','SIMULATOR_AUTO_TEST','SIMULATOR_AUTO_WAIT_FOR_PLATFORM_STOP','SIMULATOR_AUTO_RESULT_PATH','SIMULATOR_DEFAULT_API_URL','SIMULATOR_DEFAULT_DEVICE_ID','SIMULATOR_DEFAULT_DEVICE_SECRET','SIMULATOR_VOLC_INSTANCE_ID','SIMULATOR_VOLC_PRODUCT_KEY','SIMULATOR_VOLC_DEVICE_NAME','SIMULATOR_VOLC_HARDWARE_ID','SIMULATOR_VOLC_DEVICE_SECRET','SIMULATOR_VOLC_MODEL_MODE','SIMULATOR_HARDWARE_GATEWAY_URL') | ForEach-Object { Remove-Item "Env:$_" -ErrorAction SilentlyContinue } Remove-Item -LiteralPath $resultPath,$mockOutput,$mockError -Force -ErrorAction SilentlyContinue }