param( [int] $Port = 7788, [string] $Log = "$PSScriptRoot\sc_admin.log" ) # Phase-1 admin write-plane harness. Connects as the sidecar, waits for the shard, # fires admin.* commands covering the happy paths and every guard, logs the replies. # Requires Bridge.cfg AdminWriteEnabled=true and the seeded world (seed_00x accounts). function Say($msg) { for ($i = 0; $i -lt 5; $i++) { try { "$msg" | Out-File -FilePath $Log -Append -Encoding utf8; return } catch { Start-Sleep -Milliseconds 100 } } } "" | Out-File -FilePath $Log -Encoding utf8 Say "[admin] starting on 127.0.0.1:$Port" $listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port) $listener.Server.SetSocketOption('Socket', 'ReuseAddress', $true) $bound = $false for ($i = 0; $i -lt 30 -and -not $bound; $i++) { try { $listener.Start(); $bound = $true } catch { Start-Sleep -Seconds 1 } } if (-not $bound) { Say "[admin] could not bind"; exit 1 } Say "[admin] listening" $client = $listener.AcceptTcpClient() Say "[admin] === shard connected ===" $stream = $client.GetStream() $reader = New-Object System.IO.StreamReader($stream) $writer = New-Object System.IO.StreamWriter($stream) $writer.AutoFlush = $true Start-Sleep -Milliseconds 500 $requests = @( # happy path, no target needed '{"kind":"admin.broadcast","reqId":"a-bcast","actor":"whitlocktech","text":"uo-link admin test broadcast"}', # ban an offline seed account (timed), then unban '{"kind":"admin.ban","reqId":"a-ban","actor":"whitlocktech","account":"seed_001","durationSec":3600,"reason":"harness test"}', '{"kind":"admin.unban","reqId":"a-unban","actor":"whitlocktech","account":"seed_001"}', # kick an offline account -> should succeed with sessions:0 '{"kind":"admin.kick","reqId":"a-kick","actor":"whitlocktech","account":"seed_002"}', # floor: whitlocktech is Owner -> must be refused '{"kind":"admin.ban","reqId":"a-floor","actor":"whitlocktech","account":"whitlocktech"}', # unknown target '{"kind":"admin.ban","reqId":"a-unknown","actor":"whitlocktech","account":"does_not_exist"}', # missing actor -> refused by the shared gate '{"kind":"admin.ban","reqId":"a-noactor","account":"seed_003"}' ) foreach ($r in $requests) { $writer.WriteLine($r) Say "[admin] -> $r" Start-Sleep -Milliseconds 400 } # Drain greedily: block on ReadLine with an idle timeout so a buffered burst is fully read # (the DataAvailable-gated pattern drops the tail of a burst that a StreamReader pre-buffers). $stream.ReadTimeout = 2500 try { while ($true) { $line = $reader.ReadLine() if ($null -eq $line) { break } Say "[admin] <- $line" } } catch { Say "[admin] read window closed (idle)" } Say "[admin] done" $client.Close() $listener.Stop()