psmux 3.3.6

Terminal multiplexer for Windows - tmux alternative for PowerShell and Windows Terminal
# Issue #263 — Python raw-byte proof.
#
# PowerShell's [Console]::OpenStandardOutput() is intercepted by Windows
# conhost, which transforms UTF-8 bytes (E2 94 82) into 3 separate CP437
# code points (Γöé) regardless of chcp 65001. This means we have NEVER
# actually been testing what happens when a real U+2502 reaches psmux's
# cell buffer.
#
# Python's sys.stdout.buffer is a direct binary handle that calls WriteFile
# without conhost transformation. Use it to inject the exact byte sequence
# the issue is about: ESC [ <SGR> m E2 94 82 <space> <tag> ESC [ 0 m \n
#
# Verification: capture-pane -p -e re-emits the cell buffer's stored chars
# as UTF-8 + SGR. If psmux's parser stored U+2502 correctly with cell.fg,
# we will see E2 94 82 bytes preceded by the expected SGR. If psmux had a
# special-case stripping color from box-drawing chars, we would see E2 94 82
# preceded by a different (or default) SGR, while the surrounding text
# carries the requested color.

$ErrorActionPreference = "Continue"
$PSMUX = (Get-Command psmux -EA Stop).Source
$VERSION = (& $PSMUX -V).Trim()
$PY = (Get-Command python -EA Stop).Source
$SESSION = "issue263_py"
$psmuxDir = "$env:USERPROFILE\.psmux"

function Write-Pass($m) { Write-Host "  [PASS] $m" -ForegroundColor Green }
function Write-Fail($m) { Write-Host "  [FAIL] $m" -ForegroundColor Red }
function Write-Info($m) { Write-Host "  [INFO] $m" -ForegroundColor DarkCyan }

# --- Build raw bytes file with EXACT byte sequence ---
function Build-Line {
    param([string]$Sgr, [string]$Tag)
    $esc = [byte]0x1B; $lbrk = [byte]0x5B; $m = [byte]0x6D
    $box = [byte[]](0xE2, 0x94, 0x82)
    $sp = [byte]0x20; $crlf = [byte[]](0x0D, 0x0A)
    $reset = [byte[]]($esc, $lbrk, 0x30, $m)
    $sgrBytes = [System.Text.Encoding]::ASCII.GetBytes($Sgr)
    $tagBytes = [System.Text.Encoding]::ASCII.GetBytes($Tag)
    $list = New-Object System.Collections.Generic.List[byte]
    $list.Add($esc); $list.Add($lbrk)
    foreach ($b in $sgrBytes) { $list.Add($b) }
    $list.Add($m)
    foreach ($b in $box) { $list.Add($b) }
    $list.Add($sp)
    foreach ($b in $tagBytes) { $list.Add($b) }
    foreach ($b in $reset) { $list.Add($b) }
    foreach ($b in $crlf) { $list.Add($b) }
    return $list.ToArray()
}

$expected = [ordered]@{
    "BOX-090" = "90"
    "BOX-037" = "37"
    "BOX-137" = "1;37"
    "BOX-240" = "38;5;240"
    "BOX-250" = "38;5;250"
    "BOX-GRY" = "38;2;128;128;128"
    "BOX-RED" = "38;2;255;0;0"
}

$binPath = "$env:TEMP\psmux_issue263_py.bin"
$allBytes = New-Object System.Collections.Generic.List[byte]
foreach ($k in $expected.Keys) {
    $line = Build-Line -Sgr $expected[$k] -Tag $k
    foreach ($b in $line) { $allBytes.Add($b) }
}
[System.IO.File]::WriteAllBytes($binPath, $allBytes.ToArray())

$verify = [System.IO.File]::ReadAllBytes($binPath)
$verifyHex = ($verify | ForEach-Object { $_.ToString("X2") }) -join ''
$boxOccurrences = ([regex]::Matches($verifyHex, "E29482")).Count
Write-Info "Bin file: $($verify.Length) bytes, U+2502 count = $boxOccurrences"
if ($boxOccurrences -ne 7) { Write-Host "Bin file generation failed" -F Red; exit 1 }

# Python emit script
$pyEmit = "$env:TEMP\psmux_issue263_emit.py"
$pyContent = @"
import sys
with open(r'$binPath', 'rb') as f:
    data = f.read()
sys.stdout.buffer.write(data)
sys.stdout.buffer.flush()
"@
[System.IO.File]::WriteAllText($pyEmit, $pyContent, (New-Object System.Text.UTF8Encoding($false)))

# --- Cleanup ---
& $PSMUX kill-session -t $SESSION 2>&1 | Out-Null
Start-Sleep -Milliseconds 500
Remove-Item "$psmuxDir\$SESSION.*" -Force -EA SilentlyContinue

# --- Spawn pane ---
& $PSMUX new-session -d -s $SESSION -x 200 -y 60 2>$null
Start-Sleep -Seconds 3
& $PSMUX has-session -t $SESSION 2>$null
if ($LASTEXITCODE -ne 0) { Write-Host "Session creation failed" -F Red; exit 1 }

Write-Host "`n=== Issue #263 PYTHON RAW-BYTE PROOF ===" -ForegroundColor Cyan
Write-Host "  Build under test: $VERSION"
Write-Host "  Method: python sys.stdout.buffer.write() bypasses conhost"
Write-Host "  Byte path: file -> Python -> PTY -> psmux parser -> cell buffer"
Write-Host ""

# --- Force UTF-8 console (for safety, though python.buffer doesn't use it) ---
& $PSMUX send-keys -t $SESSION 'chcp 65001 | Out-Null' Enter
Start-Sleep -Milliseconds 800
& $PSMUX send-keys -t $SESSION 'Clear-Host' Enter
Start-Sleep -Seconds 1

# Run python with the emit script. -B suppresses .pyc.
& $PSMUX send-keys -t $SESSION "& '$PY' -B '$pyEmit'" Enter
Write-Info "Python emitting raw bytes to inner pane PTY, waiting 4s..."
Start-Sleep -Seconds 4

# --- Capture rendered output (cell buffer re-emitted with SGR) ---
$cap = & $PSMUX capture-pane -t $SESSION -p -e 2>&1 | Out-String

Write-Host "`n--- LIVE RENDER CAPTURE (escapes shown as \e) ---" -ForegroundColor Yellow
$ESC = [char]27
$lines = $cap -split "`r?`n"
$relevant = $lines | Where-Object { $_ -match 'BOX-' }
foreach ($l in $relevant) {
    $shown = $l -replace $ESC.ToString(), '\e'
    Write-Host "    $shown"
}

# --- Byte-level verification ---
Write-Host "`n--- BYTE-LEVEL VERIFICATION (E2 94 82 + preceding SGR) ---" -ForegroundColor Yellow

$pass = 0; $fail = 0; $missingBoxBytes = 0
foreach ($k in $expected.Keys) {
    $line = $relevant | Where-Object { $_.Contains($k) } | Select-Object -First 1
    if (-not $line) { Write-Fail "$k : line missing"; $fail++; continue }

    $bytes = [System.Text.Encoding]::UTF8.GetBytes($line)
    $hex = ($bytes | ForEach-Object { $_.ToString("X2") }) -join ''
    $boxAt = $hex.IndexOf("E29482")

    if ($boxAt -lt 0) {
        Write-Fail "$k : NO U+2502 (E2 94 82) bytes -- still mojibake"
        Write-Host "      hex=$hex" -ForegroundColor DarkGray
        $missingBoxBytes++
        $fail++
        continue
    }

    $beforeEnd = ($boxAt / 2) - 1
    $beforeBytes = $bytes[0..$beforeEnd]
    $beforeStr = [System.Text.Encoding]::UTF8.GetString($beforeBytes)
    $sgrRx = [regex]::Matches($beforeStr, "$ESC\[([^m]*)m")
    if ($sgrRx.Count -eq 0) {
        Write-Fail "$k : no SGR before E2 94 82"
        $fail++
        continue
    }
    $lastSgr = $sgrRx[$sgrRx.Count - 1].Groups[1].Value
    $wantParts = $expected[$k] -split ';'
    $lastParts = $lastSgr -split ';'
    $allFound = $true
    foreach ($wp in $wantParts) {
        if ($lastParts -notcontains $wp) { $allFound = $false; break }
    }
    if ($allFound) {
        Write-Pass "$k : E2 94 82 preceded by SGR [$lastSgr] (expected '$($expected[$k])')"
        $pass++
    } else {
        Write-Fail "$k : expected '$($expected[$k])' before E2 94 82, got [$lastSgr]"
        $fail++
    }
}

# --- Cleanup ---
& $PSMUX kill-session -t $SESSION 2>&1 | Out-Null
Remove-Item $binPath -Force -EA SilentlyContinue
Remove-Item $pyEmit -Force -EA SilentlyContinue

Write-Host "`n============================================" -ForegroundColor Cyan
Write-Host "VERDICT" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ("  U+2502 cells with correct preceding SGR: {0} / 7" -f $pass)
Write-Host ("  Failed:                                  {0} / 7" -f $fail)
Write-Host ("  (lines missing E2 94 82:                 {0})" -f $missingBoxBytes)
Write-Host ""

if ($pass -eq 7) {
    Write-Host "  >>> BUG IS NOT PRESENT in $VERSION" -ForegroundColor Green
    Write-Host "      Real U+2502 (E2 94 82) bytes reached psmux's cell buffer."
    Write-Host "      Each U+2502 cell carries its requested SGR through the"
    Write-Host "      live render output. All 7 SGR variants verify clean."
    exit 0
}
elseif ($missingBoxBytes -eq 7) {
    Write-Host "  >>> ENCODING STILL DEFEATING US" -ForegroundColor Yellow
    Write-Host "      Even Python's sys.stdout.buffer was transformed."
    Write-Host "      Need a deeper injection (Rust integration test against parser)."
    exit 2
}
elseif ($pass -gt 0 -and $missingBoxBytes -eq 0) {
    Write-Host "  >>> PARTIAL FAILURE" -ForegroundColor Red
    Write-Host "      Some U+2502 cells lost their SGR. This matches the bug."
    exit 1
}
else {
    Write-Host "  >>> MIXED RESULTS" -ForegroundColor Yellow
    exit 3
}