psmux 3.3.4

Terminal multiplexer for Windows - tmux alternative for PowerShell and Windows Terminal
<#
.SYNOPSIS
  Tests for issue #202: switch-client should actually switch the attached client to another session.
.DESCRIPTION
  Verifies that when switch-client -t <target> is sent to a psmux server,
  the server sends a SWITCH directive to the attached (persistent) client.
  This proves that switch-client is properly functional end to end.
#>

param(
    [switch]$Verbose
)

$ErrorActionPreference = 'Stop'
$script:passed = 0
$script:failed = 0

function Assert-True($condition, $message) {
    if ($condition) {
        $script:passed++
        Write-Host "  PASS: $message" -ForegroundColor Green
    } else {
        $script:failed++
        Write-Host "  FAIL: $message" -ForegroundColor Red
    }
}

function Get-SessionPort($name) {
    $portFile = "$env:USERPROFILE\.psmux\$name.port"
    if (Test-Path $portFile) {
        return [int](Get-Content $portFile).Trim()
    }
    return $null
}

function Get-SessionKey($name) {
    $keyFile = "$env:USERPROFILE\.psmux\$name.key"
    if (Test-Path $keyFile) {
        return (Get-Content $keyFile).Trim()
    }
    return ""
}

function Send-PsmuxCommand($port, $key, $command) {
    $client = New-Object System.Net.Sockets.TcpClient
    $client.Connect("127.0.0.1", $port)
    $stream = $client.GetStream()
    $writer = New-Object System.IO.StreamWriter($stream)
    $reader = New-Object System.IO.StreamReader($stream)
    $writer.AutoFlush = $true

    # Auth
    $writer.WriteLine("AUTH $key")
    $authResp = $reader.ReadLine()
    if (-not $authResp.StartsWith("OK")) {
        $client.Close()
        throw "Auth failed: $authResp"
    }

    # Send command
    $writer.WriteLine($command)
    Start-Sleep -Milliseconds 100

    $client.Close()
}

function Connect-Persistent($port, $key) {
    $client = New-Object System.Net.Sockets.TcpClient
    $client.Connect("127.0.0.1", $port)
    $client.ReceiveTimeout = 3000
    $stream = $client.GetStream()
    $writer = New-Object System.IO.StreamWriter($stream)
    $reader = New-Object System.IO.StreamReader($stream)
    $writer.AutoFlush = $true

    # Auth
    $writer.WriteLine("AUTH $key")
    $authResp = $reader.ReadLine()
    if (-not $authResp.StartsWith("OK")) {
        $client.Close()
        throw "Auth failed: $authResp"
    }

    # Enter persistent mode and attach
    $writer.WriteLine("PERSISTENT")
    $writer.WriteLine("client-attach")

    return @{ Client = $client; Writer = $writer; Reader = $reader; Stream = $stream }
}

function Read-UntilSwitch($reader, $timeoutMs = 5000) {
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    while ($sw.ElapsedMilliseconds -lt $timeoutMs) {
        try {
            $line = $reader.ReadLine()
            if ($null -eq $line) { break }
            $trimmed = $line.Trim()
            if ($trimmed.StartsWith("SWITCH ")) {
                return $trimmed
            }
        } catch {
            # Timeout on read, continue
            continue
        }
    }
    return $null
}

# ==== Setup: ensure test sessions exist ====
Write-Host "`n=== Issue #202: switch-client E2E Tests ===" -ForegroundColor Cyan

$sessA = "test-switch-alpha"
$sessB = "test-switch-beta"

# Clean up any existing test sessions
try { psmux kill-session -t $sessA 2>$null } catch {}
try { psmux kill-session -t $sessB 2>$null } catch {}
Start-Sleep -Milliseconds 500

# Create fresh test sessions
psmux new-session -d -s $sessA
Start-Sleep -Milliseconds 300
psmux new-session -d -s $sessB
Start-Sleep -Milliseconds 300

$portA = Get-SessionPort $sessA
$portB = Get-SessionPort $sessB
$keyA = Get-SessionKey $sessA
$keyB = Get-SessionKey $sessB

Assert-True ($null -ne $portA) "Session '$sessA' has a port file ($portA)"
Assert-True ($null -ne $portB) "Session '$sessB' has a port file ($portB)"

# ==== Test 1: switch-client -t from CLI returns exit 0 ====
Write-Host "`n--- Test 1: switch-client -t returns exit 0 ---"
$env:PSMUX_SESSION_NAME = $sessA
psmux switch-client -t $sessB 2>$null
Assert-True ($LASTEXITCODE -eq 0 -or $null -eq $LASTEXITCODE) "switch-client -t exits with code 0"

# ==== Test 2: SWITCH directive sent to persistent client ====
Write-Host "`n--- Test 2: SWITCH directive sent to persistent client ---"
try {
    # Connect as a persistent (attached) client to session A
    $persistent = Connect-Persistent $portA $keyA

    # Give the server time to register the persistent client
    Start-Sleep -Milliseconds 500

    # From a separate connection, send switch-client -t <sessB>
    Send-PsmuxCommand $portA $keyA "switch-client -t $sessB"

    # Read from the persistent connection - should get SWITCH directive
    $switchLine = Read-UntilSwitch $persistent.Reader 5000
    
    Assert-True ($null -ne $switchLine) "Persistent client received a SWITCH directive"
    if ($switchLine) {
        $targetSession = $switchLine.Replace("SWITCH ", "")
        Assert-True ($targetSession -eq $sessB) "SWITCH target is '$sessB' (got: '$targetSession')"
    } else {
        Write-Host "  INFO: No SWITCH directive received within timeout" -ForegroundColor Yellow
        Assert-True $false "SWITCH target matches expected session"
    }
} catch {
    Write-Host "  ERROR: $($_.Exception.Message)" -ForegroundColor Red
    Assert-True $false "Persistent client test completed without errors"
} finally {
    if ($persistent -and $persistent.Client) {
        try { $persistent.Client.Close() } catch {}
    }
}

# ==== Test 3: switch-client -n (next session) ====
Write-Host "`n--- Test 3: switch-client -n (next session) ---"
try {
    $persistent2 = Connect-Persistent $portA $keyA
    Start-Sleep -Milliseconds 500
    
    Send-PsmuxCommand $portA $keyA "switch-client -n"
    $switchLine2 = Read-UntilSwitch $persistent2.Reader 5000
    
    Assert-True ($null -ne $switchLine2) "Persistent client received SWITCH for -n"
    if ($switchLine2) {
        $target2 = $switchLine2.Replace("SWITCH ", "")
        # -n should go to the next session alphabetically after sessA
        Assert-True ($target2.Length -gt 0) "Next session target is not empty (got: '$target2')"
    }
} catch {
    Write-Host "  ERROR: $($_.Exception.Message)" -ForegroundColor Red
    Assert-True $false "switch-client -n test completed without errors"
} finally {
    if ($persistent2 -and $persistent2.Client) {
        try { $persistent2.Client.Close() } catch {}
    }
}

# ==== Test 4: switch-client -p (previous session) ====
Write-Host "`n--- Test 4: switch-client -p (prev session) ---"
try {
    $persistent3 = Connect-Persistent $portB $keyB
    Start-Sleep -Milliseconds 500
    
    Send-PsmuxCommand $portB $keyB "switch-client -p"
    $switchLine3 = Read-UntilSwitch $persistent3.Reader 5000
    
    Assert-True ($null -ne $switchLine3) "Persistent client received SWITCH for -p"
    if ($switchLine3) {
        $target3 = $switchLine3.Replace("SWITCH ", "")
        Assert-True ($target3.Length -gt 0) "Prev session target is not empty (got: '$target3')"
    }
} catch {
    Write-Host "  ERROR: $($_.Exception.Message)" -ForegroundColor Red
    Assert-True $false "switch-client -p test completed without errors"
} finally {
    if ($persistent3 -and $persistent3.Client) {
        try { $persistent3.Client.Close() } catch {}
    }
}

# ==== Test 5: switch-client -t with non-existent session shows error ====
Write-Host "`n--- Test 5: switch-client -t nonexistent session ---"
try {
    # This should NOT crash, should just fail gracefully
    $env:PSMUX_SESSION_NAME = $sessA
    psmux switch-client -t "nonexistent-session-xyz" 2>$null
    # The command is fire and forget (returns before server processes it),
    # so the exit code may vary. The important thing is it doesn't crash.
    Assert-True $true "switch-client -t nonexistent exits gracefully (no crash)"
} catch {
    Assert-True $false "switch-client with bad target should not throw"
}

# ==== Cleanup ====
Write-Host "`n--- Cleanup ---"
try { psmux kill-session -t $sessA 2>$null } catch {}
try { psmux kill-session -t $sessB 2>$null } catch {}
Start-Sleep -Milliseconds 300

# ==== Summary ====
Write-Host "`n=== Results: $script:passed passed, $script:failed failed ===" -ForegroundColor $(if ($script:failed -eq 0) { "Green" } else { "Red" })
if ($script:failed -gt 0) { exit 1 }