psmux 3.3.3

Terminal multiplexer for Windows - tmux alternative for PowerShell and Windows Terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
#!/usr/bin/env pwsh
# =============================================================================
# Test: pane split dimension limits & prompt verification
# Verifies that:
# 1. Creating splits doesn't crash the server (was crashing after ~6 splits)
# 2. The server returns an error when panes are too small to split
# 3. Every NEW pane gets a real pwsh prompt (PS C:\), verified by capture-pane
# 4. Pane count actually increases after each successful split
# 5. All pane dimensions stay >= 2x2 (ConPTY safety)
# =============================================================================

$ErrorActionPreference = 'Continue'
$PSMUX = (Resolve-Path "$PSScriptRoot\..\target\release\psmux.exe" -ErrorAction SilentlyContinue).Path
if (-not $PSMUX) { $PSMUX = (Resolve-Path "$PSScriptRoot\..\target\release\tmux.exe" -ErrorAction SilentlyContinue).Path }
if (-not $PSMUX) { Write-Error "psmux binary not found"; exit 1 }

$totalTests  = 0
$passedTests = 0
$failedTests = 0
$failures    = @()

function Log  { param([string]$msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss.fff')] $msg" }
function Pass { param([string]$name, [string]$detail)
    $script:totalTests++; $script:passedTests++
    Write-Host "  [PASS] $name - $detail" -ForegroundColor Green
}
function Fail { param([string]$name, [string]$detail)
    $script:totalTests++; $script:failedTests++
    $script:failures += "$name : $detail"
    Write-Host "  [FAIL] $name - $detail" -ForegroundColor Red
}

function Cleanup {
    try { & $PSMUX kill-server 2>&1 | Out-Null } catch {}
    Start-Sleep -Seconds 1
    try { Get-Process psmux -ErrorAction SilentlyContinue | Stop-Process -Force } catch {}
    try { Get-Process tmux  -ErrorAction SilentlyContinue | Stop-Process -Force } catch {}
    try { Get-Process pmux  -ErrorAction SilentlyContinue | Stop-Process -Force } catch {}
    Start-Sleep -Milliseconds 500
}

function Get-PaneCount {
    param([string]$Session)
    $panes = & $PSMUX list-panes -t $Session 2>&1 | Out-String
    $lines = ($panes -split "`n") | Where-Object { $_ -match '\S' }
    return $lines.Count
}

function Wait-Prompt {
    param([string]$Target, [int]$Timeout = 15000, [switch]$Relaxed)
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    while ($sw.ElapsedMilliseconds -lt $Timeout) {
        try {
            $cap = & $PSMUX capture-pane -t $Target -p 2>&1 | Out-String
            if ($cap -match "PS [A-Z]:\\") {
                return @{ Found = $true; ElapsedMs = $sw.ElapsedMilliseconds; Output = $cap }
            }
            # In very small panes the "PS " prefix wraps off screen; accept a trailing ">"
            if ($Relaxed -and $cap -match ">\s*$") {
                return @{ Found = $true; ElapsedMs = $sw.ElapsedMilliseconds; Output = $cap }
            }
        } catch {}
        Start-Sleep -Milliseconds 200
    }
    $finalCap = ""
    try { $finalCap = & $PSMUX capture-pane -t $Target -p 2>&1 | Out-String } catch {}
    return @{ Found = $false; ElapsedMs = $sw.ElapsedMilliseconds; Output = $finalCap }
}

function Check-ServerAlive {
    param([string]$Session)
    & $PSMUX has-session -t $Session 2>&1 | Out-Null
    return $LASTEXITCODE -eq 0
}

# Send a command into a pane via send-keys, wait, then capture-pane and check
# that the expected output string appears in the pane content.
function Run-And-Verify {
    param(
        [string]$Target,       # e.g. "split4:1.2"
        [string]$Command,      # e.g. "echo HELLO_MARKER"
        [string]$Expected,     # regex to match in captured output, e.g. "HELLO_MARKER"
        [int]$Timeout = 10000
    )
    # Send the command + Enter
    & $PSMUX send-keys -t $Target "$Command" Enter 2>&1 | Out-Null
    # Poll capture-pane until the expected output appears
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    while ($sw.ElapsedMilliseconds -lt $Timeout) {
        Start-Sleep -Milliseconds 300
        try {
            $cap = & $PSMUX capture-pane -t $Target -p 2>&1 | Out-String
            if ($cap -match $Expected) {
                return @{ Found = $true; ElapsedMs = $sw.ElapsedMilliseconds; Output = $cap }
            }
        } catch {}
    }
    $finalCap = ""
    try { $finalCap = & $PSMUX capture-pane -t $Target -p 2>&1 | Out-String } catch {}
    return @{ Found = $false; ElapsedMs = $sw.ElapsedMilliseconds; Output = $finalCap }
}

Log "Using: $PSMUX"
Write-Host ""

# =============================================================================
# TEST 1: Repeated vertical splits — verify each NEW pane
# =============================================================================
Write-Host ("=" * 60)
Log "TEST 1: Repeated vertical splits - verify NEW pane creation"
Write-Host ("=" * 60)
Cleanup

& $PSMUX new-session -d -s split1 2>&1 | Out-Null
Start-Sleep -Seconds 3

$r = Wait-Prompt "split1:0"
if ($r.Found) { Pass "Initial prompt" "$($r.ElapsedMs)ms" }
else          { Fail "Initial prompt" "No PS prompt in initial window" }

$maxSplits = 15
$successfulSplits = 0
$refusedSplits = 0
for ($i = 1; $i -le $maxSplits; $i++) {
    $panesBefore = Get-PaneCount "split1"
    $out = & $PSMUX split-window -t split1 -v 2>&1 | Out-String
    Start-Sleep -Milliseconds 500

    if (-not (Check-ServerAlive "split1")) {
        Fail "Server alive (vsplit $i)" "SERVER CRASHED after vsplit $i!"
        break
    }

    $panesAfter = Get-PaneCount "split1"

    # Check: did the server return an error?
    if ($out -match "too small|error|no space") {
        $refusedSplits++
        # Verify pane count did NOT increase
        if ($panesAfter -eq $panesBefore) {
            Pass "Vsplit $i refused" "correctly refused ($($out.Trim())), panes=$panesAfter"
        } else {
            Fail "Vsplit $i refused but created" "error returned but pane count went $panesBefore -> $panesAfter"
        }
        continue
    }

    # No error - split should have succeeded
    if ($panesAfter -le $panesBefore) {
        Fail "Vsplit $i no new pane" "no error but pane count unchanged ($panesBefore -> $panesAfter)"
        continue
    }

    $successfulSplits++
    $newPaneIdx = $panesAfter - 1

    # KEY CHECK: verify the NEW pane has a PS prompt
    $r = Wait-Prompt "split1:0.$newPaneIdx"
    if ($r.Found) {
        Pass "Vsplit $i new pane prompt" "pane $newPaneIdx prompt in $($r.ElapsedMs)ms (panes=$panesAfter)"
    } else {
        # Also check if any pane has the prompt — maybe active pane changed
        $anyPrompt = $false
        for ($p = 0; $p -lt $panesAfter; $p++) {
            $rc = Wait-Prompt "split1:0.$p" 3000
            if ($rc.Found -and $p -eq $newPaneIdx) { $anyPrompt = $true; break }
        }
        if ($anyPrompt) {
            Pass "Vsplit $i new pane prompt (retry)" "pane $newPaneIdx found on retry"
        } else {
            Fail "Vsplit $i new pane prompt" "pane $newPaneIdx has NO PS prompt (output: $($r.Output.Substring(0, [Math]::Min(80, $r.Output.Length))))"
        }
    }
}

if (Check-ServerAlive "split1") {
    Pass "Server survived vsplits" "$successfulSplits created, $refusedSplits refused"
} else {
    Fail "Server survived vsplits" "Server died"
}

# =============================================================================
# TEST 2: Repeated horizontal splits
# =============================================================================
Write-Host ""
Write-Host ("=" * 60)
Log "TEST 2: Repeated horizontal splits - verify NEW pane creation"
Write-Host ("=" * 60)
Cleanup

& $PSMUX new-session -d -s split2 2>&1 | Out-Null
Start-Sleep -Seconds 3

$r = Wait-Prompt "split2:0"
if ($r.Found) { Pass "Initial prompt (hsplit)" "$($r.ElapsedMs)ms" }
else          { Fail "Initial prompt (hsplit)" "No PS prompt" }

$successfulSplits = 0
$refusedSplits = 0
for ($i = 1; $i -le $maxSplits; $i++) {
    $panesBefore = Get-PaneCount "split2"
    $out = & $PSMUX split-window -t split2 -h 2>&1 | Out-String
    Start-Sleep -Milliseconds 500

    if (-not (Check-ServerAlive "split2")) {
        Fail "Server alive (hsplit $i)" "SERVER CRASHED!"
        break
    }

    $panesAfter = Get-PaneCount "split2"

    if ($out -match "too small|error|no space") {
        $refusedSplits++
        if ($panesAfter -eq $panesBefore) {
            Pass "Hsplit $i refused" "correctly refused, panes=$panesAfter"
        } else {
            Fail "Hsplit $i refused but created" "error but panes $panesBefore -> $panesAfter"
        }
        continue
    }

    if ($panesAfter -le $panesBefore) {
        Fail "Hsplit $i no new pane" "no error but panes unchanged ($panesBefore -> $panesAfter)"
        continue
    }

    $successfulSplits++
    $newPaneIdx = $panesAfter - 1

    $r = Wait-Prompt "split2:0.$newPaneIdx"
    if ($r.Found) {
        Pass "Hsplit $i new pane prompt" "pane $newPaneIdx in $($r.ElapsedMs)ms (panes=$panesAfter)"
    } else {
        Fail "Hsplit $i new pane prompt" "pane $newPaneIdx has NO PS prompt"
    }
}

if (Check-ServerAlive "split2") {
    Pass "Server survived hsplits" "$successfulSplits created, $refusedSplits refused"
} else {
    Fail "Server survived hsplits" "Server died"
}

# =============================================================================
# TEST 3: Alternating V/H splits (most realistic user scenario)
# =============================================================================
Write-Host ""
Write-Host ("=" * 60)
Log "TEST 3: Alternating V/H splits - verify every new pane"
Write-Host ("=" * 60)
Cleanup

& $PSMUX new-session -d -s split3 2>&1 | Out-Null
Start-Sleep -Seconds 3

$r = Wait-Prompt "split3:0"
if ($r.Found) { Pass "Initial prompt (alt)" "$($r.ElapsedMs)ms" }
else          { Fail "Initial prompt (alt)" "No PS prompt" }

$successfulSplits = 0
$refusedSplits = 0
for ($i = 1; $i -le 20; $i++) {
    $dir = if ($i % 2 -eq 1) { "-v" } else { "-h" }
    $dirName = if ($i % 2 -eq 1) { "V" } else { "H" }
    $panesBefore = Get-PaneCount "split3"
    $out = & $PSMUX split-window -t split3 $dir 2>&1 | Out-String
    Start-Sleep -Milliseconds 500

    if (-not (Check-ServerAlive "split3")) {
        Fail "Server alive (alt split $i $dirName)" "SERVER CRASHED!"
        break
    }

    $panesAfter = Get-PaneCount "split3"

    if ($out -match "too small|error|no space") {
        $refusedSplits++
        if ($panesAfter -eq $panesBefore) {
            Pass "AltSplit $i ($dirName) refused" "correctly refused, panes=$panesAfter"
        } else {
            Fail "AltSplit $i ($dirName) refused but created" "error but panes changed"
        }
        continue
    }

    if ($panesAfter -le $panesBefore) {
        Fail "AltSplit $i ($dirName) no new pane" "no error but panes unchanged"
        continue
    }

    $successfulSplits++
    $newPaneIdx = $panesAfter - 1

    # Later panes are very small; give extra time for shell startup
    # Panes at index >= 5 are tiny (< 15 cols); "PS " wraps off screen so use relaxed matching
    $promptTimeout = if ($newPaneIdx -ge 5) { 25000 } else { 15000 }
    $useRelaxed = $newPaneIdx -ge 5
    $r = Wait-Prompt "split3:0.$newPaneIdx" $promptTimeout -Relaxed:$useRelaxed
    if ($r.Found) {
        Pass "AltSplit $i ($dirName) new pane prompt" "pane $newPaneIdx in $($r.ElapsedMs)ms (panes=$panesAfter)"
    } else {
        Fail "AltSplit $i ($dirName) new pane prompt" "pane $newPaneIdx no prompt"
    }
}

if (Check-ServerAlive "split3") {
    Pass "Server survived alt splits" "$successfulSplits created, $refusedSplits refused"
} else {
    Fail "Server survived alt splits" "Server died"
}

# =============================================================================
# TEST 4: Multiple windows + splits (the user's exact scenario)
# =============================================================================
Write-Host ""
Write-Host ("=" * 60)
Log "TEST 4: 5 windows x 3 splits each - verify every new pane"
Write-Host ("=" * 60)
Cleanup

& $PSMUX new-session -d -s split4 2>&1 | Out-Null
Start-Sleep -Seconds 3

$r = Wait-Prompt "split4:0"
if ($r.Found) { Pass "Initial prompt (multi)" "$($r.ElapsedMs)ms" }
else          { Fail "Initial prompt (multi)" "No PS prompt" }

$totalPanes = 1
$totalRefused = 0
for ($w = 1; $w -le 4; $w++) {
    & $PSMUX new-window -t split4 2>&1 | Out-Null
    Start-Sleep -Milliseconds 1000

    if (-not (Check-ServerAlive "split4")) {
        Fail "Server alive (window $w)" "SERVER CRASHED creating window!"
        break
    }

    $r = Wait-Prompt "split4:$w"
    if ($r.Found) {
        Pass "Window $w prompt" "$($r.ElapsedMs)ms"
        $totalPanes++
    } else {
        Fail "Window $w prompt" "No prompt"
    }

    for ($s = 1; $s -le 3; $s++) {
        $dir = if ($s % 2 -eq 1) { "-v" } else { "-h" }
        $panesBefore = Get-PaneCount "split4"
        $out = & $PSMUX split-window -t split4 $dir 2>&1 | Out-String
        Start-Sleep -Milliseconds 500

        if (-not (Check-ServerAlive "split4")) {
            Fail "Server alive (win $w split $s)" "SERVER CRASHED!"
            break
        }

        $panesAfter = Get-PaneCount "split4"

        if ($out -match "too small|error|no space") {
            $totalRefused++
            Log "  Win $w split $s refused (expected)"
            continue
        }

        if ($panesAfter -le $panesBefore) {
            Fail "Win$w Split$s no new pane" "no error but panes unchanged"
            continue
        }

        $totalPanes++
        $newPaneIdx = $panesAfter - 1

        $r = Wait-Prompt "split4:$w.$newPaneIdx"
        if ($r.Found) {
            Pass "Win$w Split$s new pane prompt" "pane $newPaneIdx in $($r.ElapsedMs)ms"
        } else {
            Fail "Win$w Split$s new pane prompt" "pane $newPaneIdx has no prompt"
        }
    }
}

if (Check-ServerAlive "split4") {
    Pass "Server survived multi-window" "$totalPanes panes created, $totalRefused refused"
} else {
    Fail "Server survived multi-window" "Server died"
}

# =============================================================================
# TEST 5: Verify all pane dimensions are >= 2x2 (ConPTY safety)
# =============================================================================
Write-Host ""
Write-Host ("=" * 60)
Log "TEST 5: Verify all pane dimensions >= 2x2"
Write-Host ("=" * 60)

if (Check-ServerAlive "split4") {
    for ($w = 0; $w -le 4; $w++) {
        & $PSMUX select-window -t "split4:$w" 2>&1 | Out-Null
        Start-Sleep -Milliseconds 300
        $panes = & $PSMUX list-panes -t split4 2>&1 | Out-String
        $paneLines = ($panes -split "`n") | Where-Object { $_ -match '\S' }
        foreach ($line in $paneLines) {
            if ($line -match '\[(\d+)x(\d+)\]') {
                $cols = [int]$Matches[1]
                $rows = [int]$Matches[2]
                if ($cols -lt 2 -or $rows -lt 2) {
                    Fail "Pane dim win$w" "DANGEROUS dimensions: ${cols}x${rows} - ConPTY will crash! ($line)"
                } else {
                    Pass "Pane dim win$w" "${cols}x${rows}"
                }
            }
        }
    }
} else {
    Fail "Dimension check" "Server not alive"
}

# =============================================================================
# TEST 6: Verify exit code on refused split
# =============================================================================
Write-Host ""
Write-Host ("=" * 60)
Log "TEST 6: Exit code on refused split"
Write-Host ("=" * 60)

if (Check-ServerAlive "split4") {
    # Try to split a pane that should be too small (window 0 has been split multiple times)
    & $PSMUX select-window -t "split4:0" 2>&1 | Out-Null
    Start-Sleep -Milliseconds 300
    # Try to split the already-split panes — at least one should be too small
    # Force split into a pane we know is already at minimum
    $out = & $PSMUX split-window -t split4 -v 2>&1 | Out-String
    $exitCode = $LASTEXITCODE
    if ($out -match "too small") {
        if ($exitCode -ne 0) {
            Pass "Exit code on refuse" "exit=$exitCode, got error: $($out.Trim())"
        } else {
            Pass "Exit code on refuse" "exit=$exitCode (0 is acceptable), error: $($out.Trim())"
        }
    } else {
        # Split might have succeeded if there's still room
        Pass "Exit code test" "split succeeded (pane had room), exit=$exitCode"
    }
} else {
    Fail "Exit code test" "Server not alive"
}

# =============================================================================
# TEST 7: Run commands in every pane and verify output
# Creates a fresh session with 3 windows × 2 splits, then sends echo + ls
# into every single pane and verifies the output appeared via capture-pane.
# =============================================================================
Write-Host ""
Write-Host ("=" * 60)
Log "TEST 7: Run commands in every pane and verify output"
Write-Host ("=" * 60)
Cleanup

& $PSMUX new-session -d -s cmdtest 2>&1 | Out-Null
Start-Sleep -Seconds 3

$r = Wait-Prompt "cmdtest:0"
if ($r.Found) { Pass "Cmdtest initial prompt" "$($r.ElapsedMs)ms" }
else          { Fail "Cmdtest initial prompt" "No PS prompt" }

# Create 2 more windows, each with 2 splits (V then H) = 3 panes per window
for ($w = 1; $w -le 2; $w++) {
    & $PSMUX new-window -t cmdtest 2>&1 | Out-Null
    Start-Sleep -Milliseconds 1000
    $r = Wait-Prompt "cmdtest:$w"
    if (-not $r.Found) { Fail "Cmdtest window $w prompt" "No prompt"; continue }

    & $PSMUX split-window -t cmdtest -v 2>&1 | Out-Null
    Start-Sleep -Milliseconds 500
    & $PSMUX split-window -t cmdtest -h 2>&1 | Out-Null
    Start-Sleep -Milliseconds 500
}

# Also split the first window
& $PSMUX select-window -t "cmdtest:0" 2>&1 | Out-Null
Start-Sleep -Milliseconds 300
& $PSMUX split-window -t cmdtest -v 2>&1 | Out-Null
Start-Sleep -Milliseconds 500

# Wait for all panes to settle
Start-Sleep -Seconds 2

# Now iterate every window and every pane, run two commands:
# 1) echo MARKER_<win>_<pane>  — verify the unique marker appears
# 2) Get-ChildItem env:COMPUTERNAME — verify ls-like command works
for ($w = 0; $w -le 2; $w++) {
    & $PSMUX select-window -t "cmdtest:$w" 2>&1 | Out-Null
    Start-Sleep -Milliseconds 300

    $paneCount = Get-PaneCount "cmdtest"
    for ($p = 0; $p -lt $paneCount; $p++) {
        $target = "cmdtest:$w.$p"
        $marker = "PANE_OK_${w}_${p}"

        # First wait for the prompt to appear in this pane
        $pr = Wait-Prompt $target 8000
        if (-not $pr.Found) {
            Fail "Pane $target prompt" "No PS prompt before running command"
            continue
        }

        # TEST 7a: echo a unique marker and verify it appears
        $r = Run-And-Verify -Target $target -Command "echo $marker" -Expected $marker -Timeout 8000
        if ($r.Found) {
            Pass "echo in $target" "marker appeared in $($r.ElapsedMs)ms"
        } else {
            $snippet = if ($r.Output.Length -gt 80) { $r.Output.Substring(0, 80) } else { $r.Output }
            Fail "echo in $target" "marker '$marker' not found (captured: $snippet)"
        }

        # TEST 7b: run Get-ChildItem and verify directory listing output
        $r = Run-And-Verify -Target $target -Command "Get-ChildItem env:COMPUTERNAME" -Expected "COMPUTERNAME" -Timeout 8000
        if ($r.Found) {
            Pass "ls in $target" "Get-ChildItem output in $($r.ElapsedMs)ms"
        } else {
            $snippet = if ($r.Output.Length -gt 80) { $r.Output.Substring(0, 80) } else { $r.Output }
            Fail "ls in $target" "COMPUTERNAME not found (captured: $snippet)"
        }
    }
}

if (Check-ServerAlive "cmdtest") {
    Pass "Server survived cmd test" "all command executions completed"
} else {
    Fail "Server survived cmd test" "Server died"
}

# =============================================================================
# CLEANUP & SUMMARY
# =============================================================================
Write-Host ""
Cleanup

Write-Host ("=" * 60)
$color = if ($failedTests -eq 0) { "Green" } else { "Red" }
Write-Host "RESULTS: $passedTests passed, $failedTests failed, $totalTests total" -ForegroundColor $color
if ($failures.Count -gt 0) {
    Write-Host "Failures:" -ForegroundColor Red
    $failures | ForEach-Object { Write-Host "  - $_" -ForegroundColor Red }
}
Write-Host ("=" * 60)
exit $failedTests