# psmux GitHub Issues Reproduction Script
# Tests bugs reported in issues #25 and #19
#
# Bug 1 (Issue #25): Active window tab color not updating after select-window
# Bug 2 (Issue #19): bind-key from command prompt not working (flag stripping)
# Bug 3 (Issue #19): Status bar colors not configurable (hardcoded yellow)
#
# Run: pwsh -NoProfile -ExecutionPolicy Bypass -File tests\test_github_issues.ps1
$ErrorActionPreference = "Continue"
$script:TestsPassed = 0
$script:TestsFailed = 0
function Write-Pass { param($msg) Write-Host "[PASS] $msg" -ForegroundColor Green; $script:TestsPassed++ }
function Write-Fail { param($msg) Write-Host "[FAIL] $msg" -ForegroundColor Red; $script:TestsFailed++ }
function Write-Skip { param($msg) Write-Host "[SKIP] $msg" -ForegroundColor Yellow }
function Write-Info { param($msg) Write-Host "[INFO] $msg" -ForegroundColor Cyan }
function Write-Test { param($msg) Write-Host "[TEST] $msg" -ForegroundColor White }
$PSMUX = (Resolve-Path "$PSScriptRoot\..\target\release\psmux.exe" -ErrorAction SilentlyContinue).Path
if (-not $PSMUX) { $PSMUX = (Resolve-Path "$PSScriptRoot\..\target\debug\psmux.exe" -ErrorAction SilentlyContinue).Path }
if (-not $PSMUX) { Write-Error "psmux binary not found"; exit 1 }
Write-Info "Using: $PSMUX"
# Kill everything first
Write-Info "Cleaning up existing sessions..."
& $PSMUX kill-server 2>$null
Start-Sleep -Seconds 3
Remove-Item "$env:USERPROFILE\.psmux\*.port" -Force -ErrorAction SilentlyContinue
Remove-Item "$env:USERPROFILE\.psmux\*.key" -Force -ErrorAction SilentlyContinue
$SESSION = "issuetest"
function New-TestSession {
Start-Process -FilePath $PSMUX -ArgumentList "new-session -s $SESSION -d" -WindowStyle Hidden
Start-Sleep -Seconds 3
& $PSMUX has-session -t $SESSION 2>$null
if ($LASTEXITCODE -ne 0) { Write-Host "FATAL: Cannot create test session" -ForegroundColor Red; exit 1 }
}
function Connect-TCP {
$portFile = "$env:USERPROFILE\.psmux\$SESSION.port"
$keyFile = "$env:USERPROFILE\.psmux\$SESSION.key"
$port = [int](Get-Content $portFile).Trim()
$key = (Get-Content $keyFile).Trim()
$tcp = [System.Net.Sockets.TcpClient]::new("127.0.0.1", $port)
$tcp.NoDelay = $true
$tcp.ReceiveTimeout = 10000
$stream = $tcp.GetStream()
$enc = [System.Text.UTF8Encoding]::new($false)
$reader = [System.IO.StreamReader]::new($stream, $enc, $false, 131072)
$writer = [System.IO.StreamWriter]::new($stream, $enc, 4096)
$writer.NewLine = "`n"
$writer.AutoFlush = $false
$writer.WriteLine("AUTH $key"); $writer.Flush()
$auth = $reader.ReadLine()
if ($auth -ne "OK") { Write-Host "AUTH FAILED"; $tcp.Close(); exit 1 }
$writer.WriteLine("PERSISTENT"); $writer.Flush()
return @{ tcp = $tcp; reader = $reader; writer = $writer }
}
function Send-Fire($conn, $cmd) {
$conn.writer.WriteLine($cmd)
$conn.writer.Flush()
}
function Get-Dump($conn) {
$conn.writer.WriteLine("dump-state")
$conn.writer.Flush()
return $conn.reader.ReadLine()
}
function Get-FreshDump($conn) {
# The server pushes frames asynchronously to PERSISTENT connections
# (event-driven rendering). After Send-Fire commands, stale pushed
# frames may sit in the TCP read buffer ahead of our dump-state
# response. The real psmux client drains all frames keeping only
# the latest (client.rs line 582-610); we must do the same.
#
# Strategy: send dump-state, then drain ALL available lines (stale
# push frames + response), keeping only the last valid frame.
for ($attempt = 0; $attempt -lt 10; $attempt++) {
$conn.writer.WriteLine("dump-state")
$conn.writer.Flush()
$best = $null
$oldTimeout = $conn.tcp.ReceiveTimeout
# Use a generous timeout for the first read (server needs time
# to process), then switch to a short timeout to drain remaining
# queued frames without blocking.
$conn.tcp.ReceiveTimeout = 2000
for ($j = 0; $j -lt 200; $j++) {
try {
$line = $conn.reader.ReadLine()
} catch {
break # timeout - no more data
}
if ($null -eq $line) { break }
if ($line -ne "NC" -and $line.Length -gt 100) {
$best = $line
}
# After first valid frame, drain remaining quickly
if ($best) { $conn.tcp.ReceiveTimeout = 50 }
}
$conn.tcp.ReceiveTimeout = $oldTimeout
if ($best) { return $best }
Start-Sleep -Milliseconds 100
}
return $null
}
# ============================================================
Write-Host ""
Write-Host ("=" * 70)
Write-Host "GITHUB ISSUE REPRODUCTION TESTS"
Write-Host ("=" * 70)
# ============================================================
# BUG 1: Issue #25 - Active window tab not updating after select-window
# ============================================================
Write-Host ""
Write-Host ("=" * 70)
Write-Host "BUG 1: Active window tab not updating (Issue #25)"
Write-Host ("=" * 70)
New-TestSession
$conn = Connect-TCP
# Query base-index so we use correct window numbers regardless of config
$baseIdx = & $PSMUX show-options -t $SESSION -v base-index 2>&1
$baseIdx = [int]($baseIdx.ToString().Trim())
if ($baseIdx -lt 0 -or $baseIdx -gt 100) { $baseIdx = 0 }
Write-Info "base-index=$baseIdx (windows will be $baseIdx, $($baseIdx+1), $($baseIdx+2))"
# Create 3 windows
Send-Fire $conn "new-window"
Start-Sleep -Seconds 2
Send-Fire $conn "new-window"
Start-Sleep -Seconds 2
# Get initial state - should show last window as active (array[2])
$state1 = Get-FreshDump $conn
$json1 = $state1 | ConvertFrom-Json -ErrorAction SilentlyContinue
$activeWindows1 = @($json1.windows | Where-Object { $_.active -eq $true })
Write-Test "Three windows created, third is active"
if ($activeWindows1.Count -eq 1) {
Write-Pass "Exactly 1 active window before switch"
} else {
Write-Fail "Expected 1 active window, got $($activeWindows1.Count)"
}
# Switch to first window via select-window (base-index aware)
$firstWin = $baseIdx
Write-Test "select-window $firstWin updates active flag in dump-state"
Send-Fire $conn "select-window $firstWin"
Start-Sleep -Milliseconds 500
# Get new state
$state2 = Get-FreshDump $conn
$json2 = $state2 | ConvertFrom-Json -ErrorAction SilentlyContinue
$activeWindows2 = @($json2.windows | Where-Object { $_.active -eq $true })
if ($activeWindows2.Count -eq 1) {
$activeIdx = 0
for ($i = 0; $i -lt $json2.windows.Count; $i++) {
if ($json2.windows[$i].active) { $activeIdx = $i }
}
if ($activeIdx -eq 0) {
Write-Pass "First window (array[0]) is now active after select-window $firstWin"
} else {
Write-Fail "Active is array[$activeIdx], expected array[0] after select-window $firstWin"
}
} else {
Write-Fail "Expected 1 active window after select-window, got $($activeWindows2.Count)"
}
# Switch to second window (array index 1)
$secondWin = $baseIdx + 1
Write-Test "select-window $secondWin updates active flag"
Send-Fire $conn "select-window $secondWin"
Start-Sleep -Milliseconds 500
$state3 = Get-FreshDump $conn
$json3 = $state3 | ConvertFrom-Json -ErrorAction SilentlyContinue
$activeIdx3 = -1
for ($i = 0; $i -lt $json3.windows.Count; $i++) {
if ($json3.windows[$i].active) { $activeIdx3 = $i }
}
if ($activeIdx3 -eq 1) {
Write-Pass "Second window (array[1]) is active after select-window $secondWin"
} else {
Write-Fail "Active is array[$activeIdx3], expected array[1] after select-window $secondWin"
}
try { $conn.tcp.Close() } catch {}
& $PSMUX kill-session -t $SESSION 2>$null
Start-Sleep 2
# ============================================================
# BUG 2: Issue #19 - bind-key parsing strips command flags
# ============================================================
Write-Host ""
Write-Host ("=" * 70)
Write-Host "BUG 2: bind-key flag stripping bug (Issue #19)"
Write-Host ("=" * 70)
New-TestSession
$conn = Connect-TCP
# Test: bind-key with command that has flags
Write-Test "bind-key r split-window -h preserves -h flag"
Send-Fire $conn "bind-key r split-window -h"
Start-Sleep -Milliseconds 500
# Use CLI list-keys (reads all output correctly) instead of raw TCP ReadLine
$keys = & $PSMUX list-keys -t $SESSION 2>&1 | Out-String
if ("$keys" -match "split-window -h" -or "$keys" -match "split-window.*-h") {
Write-Pass "bind-key r: command includes -h flag"
} else {
Write-Fail "bind-key r: -h flag was stripped! Got: $keys"
}
# Test: bind-key with dash as key
Write-Test "bind-key - split-window -v (dash as key)"
Send-Fire $conn "bind-key - split-window -v"
Start-Sleep -Milliseconds 500
$keys2 = & $PSMUX list-keys -t $SESSION 2>&1 | Out-String
if ("$keys2" -match 'split-window.*-v') {
Write-Pass "bind-key -: dash key is recognized"
} else {
Write-Fail "bind-key -: dash key was treated as a flag and dropped! Got: $keys2"
}
# Test: bind-key with -T and command flags
Write-Test "bind-key -T prefix v split-window -v"
Send-Fire $conn "bind-key -T prefix v split-window -v"
Start-Sleep -Milliseconds 500
$keys3 = & $PSMUX list-keys -t $SESSION 2>&1 | Out-String
if ("$keys3" -match "split-window -v" -or "$keys3" -match "split-window.*-v") {
Write-Pass "bind-key with -T: command -v flag preserved"
} else {
Write-Fail "bind-key with -T: -v flag was stripped! Got: $keys3"
}
try { $conn.tcp.Close() } catch {}
& $PSMUX kill-session -t $SESSION 2>$null
Start-Sleep 2
# ============================================================
# BUG 3: Issue #19 - Status bar colors stuck on yellow/green
# ============================================================
Write-Host ""
Write-Host ("=" * 70)
Write-Host "BUG 3: Status bar colors not configurable (Issue #19)"
Write-Host ("=" * 70)
New-TestSession
$conn = Connect-TCP
# Get initial dump-state to check what style fields are available
$state = Get-FreshDump $conn
$json = $state | ConvertFrom-Json -ErrorAction SilentlyContinue
Write-Test "dump-state includes window-status-current-style"
$hasWscStyle = $json.PSObject.Properties.Name -contains "wsc_style"
$hasWsStyle = $json.PSObject.Properties.Name -contains "ws_style"
$hasWscstyle2 = $json.PSObject.Properties.Name -contains "window_status_current_style"
if ($hasWscStyle -or $hasWscstyle2) {
Write-Pass "dump-state has window-status-current-style field"
} else {
Write-Fail "dump-state MISSING window-status-current-style field (client can't style tabs!)"
Write-Info "Available fields: $($json.PSObject.Properties.Name -join ', ')"
}
Write-Test "dump-state includes window-status-style"
if ($hasWsStyle -or ($json.PSObject.Properties.Name -contains "window_status_style")) {
Write-Pass "dump-state has window-status-style field"
} else {
Write-Fail "dump-state MISSING window-status-style field"
}
# Test setting status-style and checking it appears
Write-Test "set status-style is reflected in dump-state"
Send-Fire $conn 'set status-style "bg=colour235 fg=colour136"'
Start-Sleep -Milliseconds 500
$state2 = Get-FreshDump $conn
$json2 = $state2 | ConvertFrom-Json -ErrorAction SilentlyContinue
if ($json2.status_style -match "colour235" -or $json2.status_style -match "235") {
Write-Pass "status-style updated in dump-state: $($json2.status_style)"
} else {
Write-Fail "status-style NOT updated in dump-state. Got: $($json2.status_style)"
}
try { $conn.tcp.Close() } catch {}
& $PSMUX kill-session -t $SESSION 2>$null
Start-Sleep 2
# ============================================================
# CLEANUP
# ============================================================
Write-Host ""
Write-Info "Final cleanup..."
& $PSMUX kill-server 2>$null
Start-Sleep 2
# ============================================================
# SUMMARY
# ============================================================
Write-Host ""
Write-Host ("=" * 70)
Write-Host "GITHUB ISSUES TEST SUMMARY" -ForegroundColor White
Write-Host ("=" * 70)
Write-Host "Passed: $($script:TestsPassed)" -ForegroundColor Green
Write-Host "Failed: $($script:TestsFailed)" -ForegroundColor $(if ($script:TestsFailed -gt 0) { "Red" } else { "Green" })
Write-Host "Total: $($script:TestsPassed + $script:TestsFailed)"
Write-Host ""
Write-Host "Bugs identified:" -ForegroundColor Yellow
Write-Host " 1. SelectWindow handler missing meta_dirty=true -> stale tab colors"
Write-Host " 2. bind-key TCP parser strips ALL '-' prefixed args including command flags"
Write-Host " 3. window-status-current-style not sent in dump-state -> client hardcodes yellow"
Write-Host ("=" * 70)
if ($script:TestsFailed -gt 0) { exit 1 }
exit 0