param( [string]$BaseUrl = "http://localhost:9900", [string]$SessionId = "mvp-demo-payment-timeout-001", [string]$RequestFile = "$PSScriptRoot/../requests/payment-timeout-chat.json", [string]$OutputDir = "$PSScriptRoot/../output" ) $ErrorActionPreference = "Stop" function ConvertFrom-NamedSse { param([Parameter(Mandatory = $true)][string]$Content) $events = @() foreach ($frame in ($Content -split "(?:\r?\n){2,}")) { if ([string]::IsNullOrWhiteSpace($frame)) { continue } $name = $null $dataLines = @() foreach ($line in ($frame -split "\r?\n")) { if ($line.StartsWith("event:")) { $name = $line.Substring(6).Trim() } elseif ($line.StartsWith("data:")) { $dataLines += $line.Substring(5).TrimStart() } } if ([string]::IsNullOrWhiteSpace($name) -or $dataLines.Count -eq 0) { throw "Invalid named SSE frame: $frame" } $rawData = $dataLines -join "`n" $events += [pscustomobject]@{ name = $name payload = $rawData | ConvertFrom-Json } } return @($events) } function Assert-ChatSseContract { param( [Parameter(Mandatory = $true)][array]$Events, [Parameter(Mandatory = $true)][string]$ExpectedSessionId ) if ($Events.Count -lt 3) { throw "Chat SSE must contain metadata, a terminal event, and done" } if ($Events[0].name -ne "metadata") { throw "First Chat SSE event must be metadata" } if ($Events[-1].name -ne "done") { throw "Last Chat SSE event must be done" } $terminalEvents = @($Events | Where-Object { $_.name -in @("content", "failure") }) if ($terminalEvents.Count -ne 1 -or $Events[-2].name -ne $terminalEvents[0].name) { throw "Chat SSE must contain exactly one content or failure immediately before done" } $allowed = @("metadata", "status", "content", "failure", "done") $unknown = @($Events | Where-Object { $_.name -notin $allowed }) if ($unknown.Count -gt 0) { throw "Chat SSE contains unknown events: $($unknown.name -join ', ')" } $invalidMiddle = @() if ($Events.Count -gt 3) { $invalidMiddle = @($Events[1..($Events.Count - 3)] | Where-Object { $_.name -ne "status" }) } if ($invalidMiddle.Count -gt 0) { throw "Only status events are allowed between metadata and the terminal event" } $metadata = $Events[0].payload if ($metadata.session_id -ne $ExpectedSessionId) { throw "SSE session_id does not match the requested SessionId" } if ([string]::IsNullOrWhiteSpace([string]$metadata.run_id)) { throw "SSE metadata is missing run_id" } $outcome = [string]$Events[-1].payload.outcome if ($terminalEvents[0].name -eq "failure" -and $outcome -ne "FAILED") { throw "A failure event must end with outcome FAILED" } if ($terminalEvents[0].name -eq "content" -and $outcome -notin @("SUCCESS", "FALLBACK")) { throw "A content event must end with outcome SUCCESS or FALLBACK" } } New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null $request = Get-Content -Raw -Encoding UTF8 -Path $RequestFile | ConvertFrom-Json $request.Id = $SessionId $body = $request | ConvertTo-Json -Depth 8 Write-Host "Running payment-timeout Chat E2E" Write-Host "BaseUrl: $BaseUrl" Write-Host "SessionId: $SessionId" $response = Invoke-WebRequest ` -UseBasicParsing ` -Method Post ` -Uri "$BaseUrl/api/chat" ` -Headers @{ Accept = "text/event-stream" } ` -ContentType "application/json; charset=utf-8" ` -Body $body $response.Content | Set-Content -Encoding UTF8 -Path "$OutputDir/chat-sse.txt" $events = @(ConvertFrom-NamedSse -Content $response.Content) Assert-ChatSseContract -Events $events -ExpectedSessionId $SessionId $events | ConvertTo-Json -Depth 50 | Set-Content -Encoding UTF8 -Path "$OutputDir/chat-events.json" $runId = [string]$events[0].payload.run_id Write-Host "RunId: $runId" Write-Host "SSE sequence: $($events.name -join ' -> ')" $trace = Invoke-RestMethod ` -Method Get ` -Uri "$BaseUrl/api/diagnosis/$SessionId/trace?runId=$([System.Uri]::EscapeDataString($runId))" $trace | ConvertTo-Json -Depth 50 | Set-Content -Encoding UTF8 -Path "$OutputDir/trace-response.json" Write-Host "E2E artifacts:" Write-Host "- $OutputDir/chat-sse.txt" Write-Host "- $OutputDir/chat-events.json" Write-Host "- $OutputDir/trace-response.json"