Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f6c3833
ui-tests: offer a more robust way to capture the terminal text
dscho Feb 20, 2026
9e561bd
fixup! Start implementing UI-based tests by adding an AutoHotKey library
dscho Feb 20, 2026
47c3440
Cygwin: pty: Fix jumbled keystrokes by removing the per-keystroke pip…
dscho Feb 26, 2026
d97d936
amend! ci: add an AutoHotKey-based integration test
dscho Feb 20, 2026
c9c17c2
Cygwin: pty: Remove pcon_start readahead flush that displaces readlin…
dscho Feb 26, 2026
c10cd37
fixup! Start implementing UI-based tests by adding an AutoHotKey library
dscho Feb 20, 2026
86a76c1
Cygwin: pty: Prevent premature pseudo console teardown that amplifies…
dscho Feb 26, 2026
cf891a0
Add AGENTS.md with comprehensive project context for AI agents
dscho Feb 20, 2026
e649a42
Merge branch 'fix-jumbled-keys'
dscho Feb 26, 2026
0b3a7f6
fixup! Start implementing UI-based tests by adding an AutoHotKey library
dscho Feb 26, 2026
53163b1
Cygwin: pty: Guard accept_input routing and flush stale readahead in …
dscho Feb 26, 2026
7b7c227
Merge branch 'add-an-AGENTS.md-file'
dscho Feb 26, 2026
467ed0c
ui-tests: add mintty launch and capture helpers to the library
dscho Feb 20, 2026
0a3ea80
Merge branch 'ahk-test-improvements'
dscho Feb 26, 2026
43dd6a3
ui-tests: add a reproducer for the keystroke reordering bug
dscho Feb 20, 2026
5e42517
Merge branch 'fix-jumbled-keys'
dscho Feb 26, 2026
7bc7b1d
ui-tests: also test keystroke order with pseudo console disabled
dscho Feb 26, 2026
fb4eb25
fixup! Add AGENTS.md with comprehensive project context for AI agents
dscho Feb 26, 2026
bd1c38c
Merge branch 'fix-jumbled-keys-with-ui-tests'
dscho Feb 26, 2026
dbfe8f8
fixup! Add AGENTS.md with comprehensive project context for AI agents
dscho Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 12 additions & 15 deletions .github/workflows/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,25 @@ jobs:
$p = Get-ChildItem -Recurse "${env:RUNNER_TEMP}\artifacts" | where {$_.Name -eq "msys-2.0.dll"} | Select -ExpandProperty VersionInfo | Select -First 1 -ExpandProperty FileName
cp $p "c:/Program Files/Git/usr/bin/msys-2.0.dll"

- uses: actions/checkout@v6
with:
sparse-checkout: |
ui-tests

- uses: actions/cache/restore@v5
id: restore-wt
with:
key: wt-${{ env.WT_VERSION }}
path: ${{ runner.temp }}/wt.zip
- name: Download Windows Terminal
if: steps.restore-wt.outputs.cache-hit != 'true'
shell: bash
- name: Install and configure portable Windows Terminal
working-directory: ui-tests
run: |
curl -fLo "$RUNNER_TEMP/wt.zip" \
https://github.com/microsoft/terminal/releases/download/v$WT_VERSION/Microsoft.WindowsTerminal_${WT_VERSION}_x64.zip
powershell -File setup-portable-wt.ps1 -WtVersion $env:WT_VERSION -DestDir $env:RUNNER_TEMP
- uses: actions/cache/save@v5
if: steps.restore-wt.outputs.cache-hit != 'true'
with:
key: wt-${{ env.WT_VERSION }}
path: ${{ runner.temp }}/wt.zip
- name: Install Windows Terminal
shell: bash
working-directory: ${{ runner.temp }}
run: |
"$WINDIR/system32/tar.exe" -xf "$RUNNER_TEMP/wt.zip" &&
cygpath -aw terminal-$WT_VERSION >>$GITHUB_PATH
- uses: actions/cache/restore@v5
id: restore-ahk
with:
Expand Down Expand Up @@ -99,10 +96,6 @@ jobs:
"$WINDIR/system32/tar.exe" -C "$RUNNER_TEMP" -xvf "$RUNNER_TEMP/win32-openssh.zip" &&
echo "OPENSSH_FOR_WINDOWS_DIRECTORY=$(cygpath -aw "$RUNNER_TEMP/OpenSSH-Win64")" >>$GITHUB_ENV

- uses: actions/checkout@v6
with:
sparse-checkout: |
ui-tests
- name: Minimize existing Log window
working-directory: ui-tests
run: |
Expand All @@ -124,13 +117,17 @@ jobs:
& "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force ctrl-c.ahk "$PWD\ctrl-c" 2>&1 | Out-Default
if (!$?) { $exitCode = 1; echo "::error::Ctrl+C Test failed!" } else { echo "::notice::Ctrl+C Test log" }
type ctrl-c.log
& "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force keystroke-order.ahk "$PWD\keystroke-order" 2>&1 | Out-Default
if (!$?) { $exitCode = 1; echo "::error::Keystroke-order Test failed!" } else { echo "::notice::Keystroke-order Test log" }
type keystroke-order.log
exit $exitCode
- name: Show logs
if: always()
working-directory: ui-tests
run: |
type bg-hook.log
type ctrl-c.log
type keystroke-order.log
- name: Take screenshot, if canceled
id: take-screenshot
if: cancelled() || failure()
Expand Down
319 changes: 319 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ui-tests/.gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.ahk eol=lf
*.ps1 eol=lf
2 changes: 1 addition & 1 deletion ui-tests/background-hook.ahk
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ WaitForRegExInWindowsTerminal('`n49$', 'Timed out waiting for commit to finish',
; Verify that CursorUp shows the previous command
Send('{Up}')
Sleep 150
Text := CaptureTextFromWindowsTerminal()
Text := CaptureBufferFromWindowsTerminal()
if not RegExMatch(Text, 'git commit --allow-empty -m zOMG *$')
ExitWithError 'Cursor Up did not work: ' Text
Info('Match!')
Expand Down
6 changes: 6 additions & 0 deletions ui-tests/cpu-stress.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
$sleepExe = & cygpath.exe -aw /usr/bin/sleep.exe
$procs = 1..[Environment]::ProcessorCount | ForEach-Object {
Start-Process -NoNewWindow -PassThru cmd.exe -ArgumentList '/c','for /L %i in (1,1,999999) do @echo . >NUL'
}
& $sleepExe 1
$procs | Stop-Process -Force -ErrorAction SilentlyContinue
26 changes: 21 additions & 5 deletions ui-tests/ctrl-c.ahk
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,27 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) {
Info('Started SSH server: ' sshdPID)

Info('Starting clone')
Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}')
Sleep 500
Info('Waiting for clone to finish')
WinActivate('ahk_id ' . hwnd)
WaitForRegExInWindowsTerminal('Receiving objects: .*, done\.`r?`nPS .*>[ `n`r]*$', 'Timed out waiting for clone to finish', 'Clone finished', 15000, 'ahk_id ' . hwnd)
retries := 5
Loop retries {
Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}')
Sleep 500
Info('Waiting for clone to finish (attempt ' . A_Index . '/' . retries . ')')
WinActivate('ahk_id ' . hwnd)
matchObj := WaitForRegExInWindowsTerminal('(Receiving objects: .*, done\.|fatal: early EOF)`r?`nPS .*>[ `n`r]*$', 'Timed out waiting for clone to finish', 'Clone command completed', 15000, 'ahk_id ' . hwnd)

if InStr(matchObj[1], 'done.')
break
if A_Index == retries
ExitWithError('Clone failed after ' . retries . ' attempts (early EOF)')
Info('Clone failed (early EOF), restarting SSH server and retrying...')
if DirExist(largeGitClonePath)
DirDelete(largeGitClonePath, true)
; Restart sshd for the next attempt (it may have exited after the failed connection)
Run(openSSHPath . '\sshd.exe ' . sshdOptions, '', 'Hide', &sshdPID)
if A_LastError
ExitWithError 'Error restarting SSH server: ' A_LastError
Info('Restarted SSH server: ' sshdPID)
}

if not DirExist(largeGitClonePath)
ExitWithError('`large-clone` did not work?!?')
Expand Down
165 changes: 165 additions & 0 deletions ui-tests/keystroke-order.ahk
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#Requires AutoHotkey v2.0
#Include ui-test-library.ahk

; Reproducer for https://github.com/git-for-windows/git/issues/5632
;
; Keystroke reordering: when a non-MSYS2 process runs in the foreground
; of a PTY, keystrokes typed into bash arrive out of order because the
; MSYS2 runtime's transfer_input() can reorder bytes across pipe buffers.
;
; The test types characters interleaved with backspaces while a non-MSYS
; foreground process (powershell launching MSYS sleep) runs under CPU
; stress. If backspace bytes get reordered relative to the characters
; they should delete, readline produces wrong output.
;
; The test runs in two phases:
; Phase 1 (pcon enabled): the default mode, exercises the pseudo
; console oscillation code paths in master::write().
; Phase 2 (disable_pcon): sets MSYS=disable_pcon so that pseudo
; console is never created, exercising the non-pcon input routing
; and verifying that typeahead is preserved correctly.

SetWorkTree('git-test-keystroke-order')

testString := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'

hwnd := LaunchMintty()
winId := 'ahk_id ' hwnd

; Wait for bash prompt via HTML export (Ctrl+F5).
deadline := A_TickCount + 60000
while A_TickCount < deadline
{
capture := CaptureBufferFromMintty(winId)
if InStr(capture, '$ ')
break
Sleep 500
}
if !InStr(capture, '$ ')
ExitWithError 'Timed out waiting for bash prompt'
Info 'Bash prompt appeared'

stressCmd := 'powershell.exe -File ' StrReplace(A_ScriptDir, '\', '/') '/cpu-stress.ps1'
Info 'Foreground command: ' stressCmd

; === Phase 1: pcon enabled (default) ===
Info '=== Phase 1: pcon enabled ==='
mismatch := RunKeystrokeTest(winId, stressCmd, testString, 20)

if !mismatch
{
; === Phase 2: disable_pcon ===
Info '=== Phase 2: disable_pcon ==='
WinActivate(winId)
SetKeyDelay 20, 20
SendEvent('{Text}export MSYS=disable_pcon')
SendEvent('{Enter}')
Sleep 500

mismatch := RunKeystrokeTest(winId, stressCmd, testString, 5)
}

WinActivate(winId)
SetKeyDelay 20, 20
Send '{Ctrl down}c{Ctrl up}'
Sleep 500
SendEvent('{Text}exit')
SendEvent('{Enter}')
Sleep 1000
ExitApp mismatch ? 1 : 0

; Run the keystroke reordering test for a given number of iterations.
; Returns true if a mismatch was detected, false if all iterations passed.
RunKeystrokeTest(winId, stressCmd, testString, maxIterations) {
mismatch := false
chunkSize := 2

Loop maxIterations
{
iteration := A_Index
Info 'Iteration ' iteration ' of ' maxIterations

WinActivate(winId)

; 1. Launch foreground stress process
SetKeyDelay 20, 20
SendEvent('{Text}' stressCmd)
SendEvent('{Enter}')

; 2. Type with backspaces: send chunkSize chars + "XY" + BS*2 at a time.
SetKeyDelay 1, 1
Sleep 500
offset := 1
while offset <= StrLen(testString)
{
chunk := SubStr(testString, offset, chunkSize)
SendEvent('{Text}' chunk 'XY')
SendEvent('{Backspace}{Backspace}')
offset += chunkSize
}

; 3. Poll the HTML export for what readline rendered after "$ ".
; The HTML shows the final screen state (backspaces already applied).
Sleep 2000
deadline := A_TickCount + 30000
while A_TickCount < deadline
{
text := CaptureBufferFromMintty(winId)

; Find the last "$ " and extract the text after it
lastPrompt := 0
pos := 1
while pos := InStr(text, '$ ', , pos)
{
lastPrompt := pos
pos += 2
}
if lastPrompt > 0
{
after := Trim(SubStr(text, lastPrompt + 2))
; Take first "word" (up to whitespace or end)
spPos := InStr(after, ' ')
if spPos > 0
after := SubStr(after, 1, spPos - 1)

if after = testString
{
Info 'Iteration ' iteration ': OK'
break
}
if InStr(after, 'powershell') || InStr(after, 'sleep') || after = ''
{
; Stress command or bare prompt -- keep waiting
}
else if SubStr(testString, 1, StrLen(after)) != after
{
Info 'MISMATCH in iteration ' iteration '!'
Info 'Expected: ' testString
Info 'Got: ' after
mismatch := true
break
}
}
Sleep 500
}

if A_TickCount >= deadline
{
Info 'TIMEOUT in iteration ' iteration
mismatch := true
break
}
if mismatch
break

; Clear readline buffer for next iteration
SetKeyDelay 20, 20
Send '{Ctrl down}u{Ctrl up}'
Sleep 300
}

if !mismatch
Info 'All ' maxIterations ' iterations passed'

return mismatch
}
96 changes: 96 additions & 0 deletions ui-tests/setup-portable-wt.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Configures a portable Windows Terminal for the UI tests.
#
# Downloads WT if needed, then creates .portable marker and settings.json
# with exportBuffer bound to Ctrl+Shift+F12. The export file lands in the
# script's own directory (ui-tests/) so it gets uploaded as build artifact.
#
# The portable WT uses its own settings directory (next to the executable)
# so it never touches the user's installed Windows Terminal configuration.

param(
[string]$WtVersion = $env:WT_VERSION,
[string]$DestDir = $env:TEMP
)

if (-not $WtVersion) { $WtVersion = '1.22.11141.0' }

$wtDir = "$DestDir\terminal-$WtVersion"
$wtExe = "$wtDir\wt.exe"

# Download if the directory doesn't contain wt.exe yet
if (-not (Test-Path $wtExe)) {
$wtZip = "$DestDir\wt.zip"
if (-not (Test-Path $wtZip)) {
$url = "https://github.com/microsoft/terminal/releases/download/v$WtVersion/Microsoft.WindowsTerminal_${WtVersion}_x64.zip"
Write-Host "Downloading Windows Terminal $WtVersion ..."
curl.exe -fLo $wtZip $url
if ($LASTEXITCODE -ne 0) { throw "Download failed" }
}
Write-Host "Extracting ..."
& "$env:WINDIR\system32\tar.exe" -C $DestDir -xf $wtZip
if ($LASTEXITCODE -ne 0) { throw "Extract failed" }
}

# Create .portable marker so WT reads settings from settings\ next to wt.exe
$portableMarker = "$wtDir\.portable"
if (-not (Test-Path $portableMarker)) {
Set-Content -Path $portableMarker -Value ""
}

# Write settings.json with exportBuffer action
$settingsDir = "$wtDir\settings"
if (-not (Test-Path $settingsDir)) { New-Item -ItemType Directory -Path $settingsDir -Force | Out-Null }

$bufferExportPath = ($PSScriptRoot + '\wt-buffer-export.txt') -replace '\\', '/'

$settings = @"
{
"`$schema": "https://aka.ms/terminal-profiles-schema",
"actions": [
{
"command": {
"action": "exportBuffer",
"path": "$bufferExportPath"
},
"id": "User.TestExportBuffer"
},
{
"command": { "action": "copy", "singleLine": false },
"id": "User.copy"
},
{ "command": "paste", "id": "User.paste" }
],
"copyFormatting": "none",
"copyOnSelect": false,
"defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"keybindings": [
{ "id": "User.TestExportBuffer", "keys": "ctrl+shift+f12" },
{ "id": null, "keys": "ctrl+v" },
{ "id": null, "keys": "ctrl+c" }
],
"profiles": {
"defaults": {},
"list": [
{
"commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"hidden": false,
"name": "Windows PowerShell"
}
]
},
"schemes": [],
"themes": []
}
"@

Set-Content -Path "$settingsDir\settings.json" -Value $settings

# Add WT to PATH if running in GitHub Actions
if ($env:GITHUB_PATH) {
$wtDir | Out-File -Append -FilePath $env:GITHUB_PATH
}

Write-Host "Portable WT ready at: $wtDir"
Write-Host " exportBuffer path: $bufferExportPath"
Write-Host " exportBuffer key: Ctrl+Shift+F12"
Loading
Loading