psmux 3.3.6

Terminal multiplexer for Windows - tmux alternative for PowerShell and Windows Terminal
# Issue #88 verification — does `capture-pane -S` reliably retrieve
# scrollback that the user produced inside a psmux pane?
#
# Three distinct tests, each isolating one variable:
#
#   T1. Plain stdout (no TUI): 200 lines via PowerShell pipeline.
#       Baseline — if this fails, scrollback is broken end-to-end.
#
#   T2. TUI app using the alternate screen.  Output produced while
#       the alt screen is active should NOT land in main-grid
#       scrollback (this is correct vt100/tmux semantics).  Output
#       printed BEFORE entering alt screen, or AFTER exiting, MUST
#       still be in scrollback.  Tests whether we corrupt main
#       scrollback during alt-screen excursions.
#
#   T3. Codex itself, exact CXwudi scenario.  Run `codex exec`
#       (non-interactive — prints to stdout, no alt screen) and
#       ask it to emit 200 numbered lines.  Then capture-pane -S
#       must return all 200.  This is the literal repro from the
#       most recent issue comment.

$ErrorActionPreference = "Continue"
$PSMUX = (Get-Command psmux -EA Stop).Source
$psmuxDir = "$env:USERPROFILE\.psmux"
$script:TestsPassed = 0
$script:TestsFailed = 0

function Write-Pass($msg) { Write-Host "  [PASS] $msg" -ForegroundColor Green; $script:TestsPassed++ }
function Write-Fail($msg) { Write-Host "  [FAIL] $msg" -ForegroundColor Red; $script:TestsFailed++ }
function Write-Info($msg) { Write-Host "  [INFO] $msg" -ForegroundColor DarkCyan }

function Wait-Prompt {
    param([string]$Target, [int]$TimeoutMs = 15000, [string]$Pattern = "PS [A-Z]:\\")
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    while ($sw.ElapsedMilliseconds -lt $TimeoutMs) {
        try {
            $cap = & $PSMUX capture-pane -t $Target -p 2>&1 | Out-String
            if ($cap -match $Pattern) { return $true }
        } catch {}
        Start-Sleep -Milliseconds 200
    }
    return $false
}

function Wait-Output {
    param([string]$Target, [string]$Marker, [int]$TimeoutMs = 60000)
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    while ($sw.ElapsedMilliseconds -lt $TimeoutMs) {
        $cap = & $PSMUX capture-pane -t $Target -p 2>&1 | Out-String
        if ($cap -match $Marker) { return $true }
        Start-Sleep -Milliseconds 300
    }
    return $false
}

function Reset-Server {
    & $PSMUX kill-server 2>&1 | Out-Null
    Start-Sleep -Seconds 1
    Remove-Item "$psmuxDir\*.port","$psmuxDir\*.key","$psmuxDir\*.sess" -Force -EA SilentlyContinue
}

# ── T1: PLAIN STDOUT (200 lines) ───────────────────────────────────
Write-Host "`n=== T1: plain 200-line stdout, capture-pane -S -1000 ===" -ForegroundColor Cyan
Reset-Server
$SESSION = "iss88_plain"
& $PSMUX new-session -d -s $SESSION 2>&1 | Out-Null
Start-Sleep -Seconds 4

if (-not (Wait-Prompt -Target $SESSION)) {
    Write-Fail "T1: shell not ready"
} else {
    Write-Pass "T1: shell ready"
    # Emit 200 numbered lines through PowerShell.
    & $PSMUX send-keys -t $SESSION '1..200 | ForEach-Object { "plain $_" }' Enter 2>&1 | Out-Null
    if (Wait-Output -Target $SESSION -Marker "plain 199" -TimeoutMs 30000) {
        Start-Sleep -Seconds 1
        # Capture deep scrollback (-S -1000 = 1000 rows back from top of view).
        $deep = & $PSMUX capture-pane -t $SESSION -S -1000 -p 2>&1 | Out-String
        $matches = [regex]::Matches($deep, '(?m)^plain (\d+)\b')
        $count = $matches.Count
        $nums = $matches | ForEach-Object { [int]$_.Groups[1].Value }
        if ($count -gt 0) {
            $min = ($nums | Measure-Object -Minimum).Minimum
            $max = ($nums | Measure-Object -Maximum).Maximum
            Write-Info "T1: captured $count lines, range [$min..$max]"
        } else {
            Write-Info "T1: captured 0 'plain N' lines"
        }
        if ($count -ge 195) {
            Write-Pass "T1: capture-pane -S -1000 returns all 200 lines (got $count)"
        } else {
            Write-Fail "T1: capture-pane -S -1000 missed lines (got $count of 200)"
        }
    } else {
        Write-Fail "T1: 200 lines did not appear in pane"
    }
}
& $PSMUX kill-session -t $SESSION 2>&1 | Out-Null
Reset-Server

# ── T2: TUI APP (alt screen) ────────────────────────────────────────
# Use `more` (built-in pager): it does NOT switch to alt-screen so
# its output should land in scrollback normally.  Then use `vim`
# style: the alt-screen behaviour of TUI apps is well-known and
# we want a predictable test.  Skip if vim not available.
Write-Host "`n=== T2: pre/post-TUI scrollback survives alt-screen excursion ===" -ForegroundColor Cyan
Reset-Server
$SESSION = "iss88_tui"
& $PSMUX new-session -d -s $SESSION 2>&1 | Out-Null
Start-Sleep -Seconds 4
if (-not (Wait-Prompt -Target $SESSION)) {
    Write-Fail "T2: shell not ready"
} else {
    Write-Pass "T2: shell ready"
    # Step 1: emit 50 lines BEFORE any alt-screen excursion.
    & $PSMUX send-keys -t $SESSION '1..50 | ForEach-Object { "pre $_" }' Enter 2>&1 | Out-Null
    if (Wait-Output -Target $SESSION -Marker "pre 49" -TimeoutMs 30000) {
        Start-Sleep -Seconds 1
        # Step 2: emit raw alt-screen enter+exit via printf-style escape.
        # ESC[?1049h enter alt screen, ESC[?1049l exit.  This simulates
        # what a TUI app does without needing one installed.
        & $PSMUX send-keys -t $SESSION ([char]27 + "[?1049h" + "TUI_VISIBLE_TEXT" + [char]27 + "[?1049l") 2>&1 | Out-Null
        Start-Sleep -Seconds 1
        # Step 3: emit 50 more lines AFTER exiting alt screen.
        & $PSMUX send-keys -t $SESSION '1..50 | ForEach-Object { "post $_" }' Enter 2>&1 | Out-Null
        if (Wait-Output -Target $SESSION -Marker "post 49" -TimeoutMs 30000) {
            Start-Sleep -Seconds 1
            $deep = & $PSMUX capture-pane -t $SESSION -S -2000 -p 2>&1 | Out-String
            $preCount = ([regex]::Matches($deep, '(?m)^pre (\d+)\b')).Count
            $postCount = ([regex]::Matches($deep, '(?m)^post (\d+)\b')).Count
            Write-Info "T2: pre-alt lines retained: $preCount of 50, post-alt: $postCount of 50"
            if ($preCount -ge 45 -and $postCount -ge 45) {
                Write-Pass "T2: scrollback survives alt-screen enter/exit"
            } else {
                Write-Fail "T2: scrollback corrupted by alt-screen excursion (pre=$preCount post=$postCount)"
            }
        }
    }
}
& $PSMUX kill-session -t $SESSION 2>&1 | Out-Null
Reset-Server

# ── T3: CODEX EXEC — exact CXwudi scenario ──────────────────────────
Write-Host "`n=== T3: CXwudi's literal repro — codex exec emits 200 lines ===" -ForegroundColor Cyan
$codexExe = (Get-Command codex -EA SilentlyContinue).Source
if (-not $codexExe) {
    Write-Info "T3: skipping — codex not found on PATH"
} else {
    Reset-Server
    $SESSION = "iss88_codex"
    & $PSMUX new-session -d -s $SESSION 2>&1 | Out-Null
    Start-Sleep -Seconds 4
    if (-not (Wait-Prompt -Target $SESSION)) {
        Write-Fail "T3: shell not ready"
    } else {
        Write-Pass "T3: shell ready"
        # `codex exec` runs non-interactively and writes to stdout — no
        # alt screen.  Ask it to print 200 numbered lines.  We use a
        # deterministic prompt that asks for plain stdout, no tooling.
        # Use a marker that's unique enough to grep without false hits.
        $prompt = "Print exactly 200 lines of the form 'codex line N' where N is 1 to 200, one per line, nothing else, no commentary, no markdown."
        Write-Info "T3: launching codex exec inside pane (this may take 30-60s)..."
        # Use single quotes around the prompt; PowerShell will not
        # interpolate, and the prompt is sent verbatim to send-keys.
        & $PSMUX send-keys -t $SESSION "Set-Location 'c:\cctest'" Enter 2>&1 | Out-Null
        Start-Sleep -Milliseconds 500
        & $PSMUX send-keys -t $SESSION ("codex exec --skip-git-repo-check `"$prompt`"") Enter 2>&1 | Out-Null

        # Wait for codex to finish (look for the last marker).  Give
        # it up to 3 minutes since the model latency is the wild card.
        if (Wait-Output -Target $SESSION -Marker "codex line 199" -TimeoutMs 240000) {
            Start-Sleep -Seconds 3
            $deep = & $PSMUX capture-pane -t $SESSION -S -2000 -p 2>&1 | Out-String
            $matches = [regex]::Matches($deep, '(?m)^codex line (\d+)\b')
            $count = $matches.Count
            if ($count -gt 0) {
                $nums = $matches | ForEach-Object { [int]$_.Groups[1].Value }
                $min = ($nums | Measure-Object -Minimum).Minimum
                $max = ($nums | Measure-Object -Maximum).Maximum
                Write-Info "T3: captured $count codex lines, range [$min..$max]"
            }
            if ($count -ge 195) {
                Write-Pass "T3: codex exec output survives in scrollback ($count of 200)"
            } else {
                Write-Fail "T3: BUG CONFIRMED — only $count of 200 codex lines retained"
            }
        } else {
            Write-Info "T3: codex did not finish within 4 minutes (skipping count assertion)"
            $cap = & $PSMUX capture-pane -t $SESSION -p 2>&1 | Out-String
            $tail = $cap.Substring([Math]::Max(0, $cap.Length - 500))
            Write-Info "T3: pane tail:`n$tail"
        }
    }
    & $PSMUX kill-session -t $SESSION 2>&1 | Out-Null
    Reset-Server
}

Write-Host "`n=== Results ===" -ForegroundColor Cyan
Write-Host "  Passed: $($script:TestsPassed)" -ForegroundColor Green
Write-Host "  Failed: $($script:TestsFailed)" -ForegroundColor $(if ($script:TestsFailed -gt 0) { "Red" } else { "Green" })
exit $script:TestsFailed