Public/Test-AnonymousDebateFlow.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Test-AnonymousDebateFlow { <# .SYNOPSIS End-to-end smoke test of the anonymous user journey against a deployed Taxonomy Editor. .DESCRIPTION Exercises the seven API steps an anonymous (free-tier) user touches when running a debate end-to-end: 1. POST /api/auth/anonymous — create session 2. POST /api/ai/generate — small prompt 3. POST /api/embeddings/compute — short text 4. PUT /api/debates/{id} — minimal debate JSON 5. POST /api/flight-recorder/server-dump — trigger dump 6. GET /api/flight-recorder/download-merged/{dumpId} — download 7. POST /api/embeddings/query — semantic search (t/1171) Steps 2-7 share the session cookie issued by step 1. Step 6 uses the dumpId returned from step 5 (falls back to a sentinel if absent so the request still exercises the endpoint). This is the regression net for the cluster of production bugs from 2026-06-26 (t/1060 ENOENT on session store, t/1061 budget exhaustion, t/1062 anonymous 403 on embeddings, t/1064 admin-only flight recorder gate). All five would have been caught by a single run of this cmdlet. .PARAMETER BaseUrl Target server URL. Defaults to the current production URL. .PARAMETER Detailed Print per-step timing + response body snippets to host (in addition to the structured table result). .PARAMETER StopOnFailure Halt at the first failing step. Default is to run all six and report. .PARAMETER TimeoutSec Per-request HTTP timeout (1-120, default 30 — AI generate can be slow). .EXAMPLE Test-AnonymousDebateFlow .EXAMPLE Test-AnonymousDebateFlow -BaseUrl https://staging.example.com -Detailed .EXAMPLE Test-AnonymousDebateFlow -StopOnFailure | Where-Object { -not $_.Pass } .LINK Show-AITriadHelp .LINK Test-TaxEditorHealth .LINK Test-TaxEditorEndpoints .LINK Test-PersonaEndpoints .LINK Test-ServiceWorkerHealth .LINK Get-FreeTierStatus .LINK Invoke-TaxEditorSmokeTest #> [CmdletBinding()] [OutputType([AnonymousFlowStepResult[]])] param( [Parameter(Position = 0)] [string]$BaseUrl = (Get-TaxEditorBaseUrl), [switch]$Detailed, [switch]$StopOnFailure, [ValidateRange(1, 120)] [int]$TimeoutSec = 30 ) Set-StrictMode -Version Latest $BaseUrl = $BaseUrl.TrimEnd('/') $Session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() $TotalSw = [System.Diagnostics.Stopwatch]::StartNew() # Step descriptors (Method, Path, Body, AcceptableStatusCodes, Description, BugTags) # Path is built lazily because steps 4 + 6 depend on prior step output. $DebateId = [guid]::NewGuid().ToString() $StepDefs = @( @{ Step = 1; Method = 'POST'; Path = '/api/auth/anonymous'; Body = @{}; OkCodes = @(200, 201); Desc = 'Create anonymous session'; BugTags = 't/1060' PostHook = $null } @{ Step = 2; Method = 'POST'; Path = '/api/ai/generate'; Body = @{ prompt = 'Reply with the word OK.'; model = 'gemini-3.5-flash-lite'; maxTokens = 8 }; OkCodes = @(200); Desc = 'AI generate (free tier)'; BugTags = 't/1061 t/1062' PostHook = $null } @{ Step = 3; Method = 'POST'; Path = '/api/embeddings/compute'; Body = @{ texts = @('smoke test') }; OkCodes = @(200); Desc = 'Embeddings compute (anon)'; BugTags = 't/1062' PostHook = $null } @{ Step = 4; Method = 'PUT'; Path = "/api/debates/$DebateId"; Body = @{ id = $DebateId; title = 'smoke-anon'; transcript = @() }; OkCodes = @(200, 201, 204); Desc = 'Save debate (session-backed)'; BugTags = 't/1060' PostHook = $null } @{ Step = 5; Method = 'POST'; Path = '/api/flight-recorder/server-dump'; Body = @{ reason = 'Test-AnonymousDebateFlow smoke' }; OkCodes = @(200, 201, 202); Desc = 'Trigger flight-recorder dump'; BugTags = 't/1064' PostHook = { param($Body) if ($Body -and $Body.PSObject.Properties['dumpId']) { return $Body.dumpId } if ($Body -and $Body.PSObject.Properties['id']) { return $Body.id } return $null } } @{ Step = 6; Method = 'GET'; Path = $null; # filled in below from step 5's dumpId Body = $null; OkCodes = @(200); Desc = 'Download merged dump'; BugTags = 't/1064' PostHook = $null } @{ Step = 7; Method = 'POST'; Path = '/api/embeddings/query'; Body = @{ text = 'free-tier semantic search smoke' }; OkCodes = @(200); Desc = 'Embeddings query (anon, semantic search)'; BugTags = 't/1171' PostHook = $null } ) $Results = [System.Collections.Generic.List[AnonymousFlowStepResult]]::new() $DumpId = $null foreach ($Def in $StepDefs) { # Late-bind step 6's path from step 5's dumpId if ($Def.Step -eq 6) { $Def.Path = "/api/flight-recorder/download-merged/$(if ($DumpId) { $DumpId } else { 'unknown' })" } Write-Verbose "[$($Def.Step)/7] $($Def.Desc) → $($Def.Method) $($Def.Path)" $Params = @{ BaseUrl = $BaseUrl Path = $Def.Path Method = $Def.Method TimeoutSec = $TimeoutSec AcceptableStatusCodes = $Def.OkCodes Session = $Session } if ($null -ne $Def.Body) { $Params.Body = $Def.Body } $Check = Invoke-RemoteCheck @Params # Capture dumpId from step 5 for step 6 if ($Def.Step -eq 5 -and $Check.Success -and $Def.PostHook) { $DumpId = & $Def.PostHook $Check.Body if (-not $DumpId) { Write-Verbose " step 5 succeeded but response had no dumpId/id — step 6 will request 'unknown'" } else { Write-Verbose " captured dumpId=$DumpId for step 6" } } $R = [AnonymousFlowStepResult]::new() $R.Step = $Def.Step $R.Method = $Def.Method $R.Endpoint = $Def.Path $R.Description = $Def.Desc $R.BugTags = $Def.BugTags $R.Pass = $Check.Success $R.StatusCode = $Check.StatusCode $R.Ms = $Check.ResponseMs $R.Error = $Check.Error $Results.Add($R) if ($Detailed) { $Status = if ($R.Pass) { 'PASS' } else { 'FAIL' } $Color = if ($R.Pass) { 'Green' } else { 'Red' } Write-Host (" [{0}/7] {1,-4} {2,-44} {3,5} ms [{4}]" -f $Def.Step, $Status, $Def.Path, $R.Ms, $R.StatusCode) -ForegroundColor $Color if (-not $R.Pass -and $R.Error) { Write-Host " $($R.Error)" -ForegroundColor DarkGray } } if ($StopOnFailure -and -not $R.Pass) { Write-Verbose "Stopping at step $($Def.Step) due to -StopOnFailure" break } } $TotalSw.Stop() $Passed = @($Results | Where-Object { $_.Pass }).Count $Failed = @($Results | Where-Object { -not $_.Pass }).Count $Color = if ($Failed -eq 0) { 'Green' } else { 'Red' } Write-Host ("Anonymous flow: {0} passed, {1} failed ({2:n1}s elapsed)" -f $Passed, $Failed, ($TotalSw.ElapsedMilliseconds / 1000.0)) -ForegroundColor $Color @($Results) } |