Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
754554c
ui-tests: offer a more robust way to capture the terminal text
dscho Feb 20, 2026
5061115
fixup! Start implementing UI-based tests by adding an AutoHotKey library
dscho Feb 20, 2026
60caca8
amend! ci: add an AutoHotKey-based integration test
dscho Feb 20, 2026
370c3a6
fixup! Start implementing UI-based tests by adding an AutoHotKey library
dscho Feb 20, 2026
1e0ff37
Add AGENTS.md with comprehensive project context for AI agents
dscho Feb 20, 2026
0e6438b
fixup! Start implementing UI-based tests by adding an AutoHotKey library
dscho Feb 26, 2026
78d4be1
amend! Start implementing UI-based tests by adding an AutoHotKey library
dscho Mar 6, 2026
056581b
Merge branch 'add-an-AGENTS.md-file'
dscho Feb 26, 2026
aff073a
Merge branch 'ahk-test-improvements'
dscho Feb 26, 2026
3d8be6b
Cygwin: pty: Fix nat pipe hand-over when pcon is disabled
tyan0 Mar 3, 2026
a0f8f2b
Cygwin: console: Release pipe_sw_mutex in pcon_hand_over_proc()
tyan0 Mar 24, 2026
79ae2b0
Cygwin: pty: Fix input transfer when multiple non-cygwin apps exist
tyan0 Mar 28, 2026
10aff7d
Cygwin: console: Fix master thread
tyan0 Mar 28, 2026
0c28b92
fixup! ci: add an AutoHotKey-based integration test
dscho Mar 6, 2026
dc927cd
fixup! ui-tests: minimize Log window
dscho Mar 6, 2026
50c1ac0
fixup! ui-tests: verify that a `sleep` in Windows Terminal can be int…
dscho Mar 6, 2026
c64eb65
Merge branch 'write-directly-to-ci-log-in-ui-tests'
dscho Mar 6, 2026
4f08a75
Cygwin: pty: Add workaround for handling of backspace when pcon enabled
tyan0 Mar 28, 2026
b9d7627
Cygwin: console: Use input_mutex in the parent PTY in master thread
tyan0 Mar 28, 2026
15c111b
Cygwin: pty: Apply line_edit() for transferred input to to_cyg
tyan0 Mar 28, 2026
9e78b35
Cygwin: pty: Guard get_winpid_to_hand_over() with attach_mutex
tyan0 Mar 28, 2026
d235511
Cygwin: pty: Guard to_be_read_from_nat_pipe() by pipe_sw_mutex
tyan0 Mar 28, 2026
76f644d
Cygwin: pty: Drop nat_fg() check from to_be_read_from_nat_pipe()
tyan0 Mar 28, 2026
4ad886b
Merge branch 'fix-jumbled-keys'
dscho Feb 26, 2026
59ea95a
ui-tests: add mintty launch and capture helpers to the library
dscho Feb 20, 2026
daa6f61
ui-tests: add a reproducer for the keystroke reordering bug
dscho Feb 20, 2026
1aa5bf8
squash! ui-tests: add a reproducer for the keystroke reordering bug
dscho Feb 26, 2026
fbdb6fd
squash! ui-tests: add a reproducer for the keystroke reordering bug
dscho Mar 6, 2026
283fa8c
squash! ui-tests: add a reproducer for the keystroke reordering bug
dscho Mar 28, 2026
85c357e
Merge branch 'fix-jumbled-keys'
dscho Feb 26, 2026
4dff1e2
fixup! Add AGENTS.md with comprehensive project context for AI agents
dscho Feb 26, 2026
1a3b0a5
Merge branch 'fix-jumbled-keys-with-ui-tests'
dscho Feb 26, 2026
6c1ab7b
fixup! Add AGENTS.md with comprehensive project context for AI agents
dscho Feb 26, 2026
fb42d71
fixup! Add AGENTS.md with comprehensive project context for AI agents
dscho Mar 27, 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
29 changes: 11 additions & 18 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,15 +96,10 @@ 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: |
$exitCode = 0
type minimize-log-window.ahk
& "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force minimize-log-window.ahk "$PWD\minimize-log-window" 2>&1 | Out-Default
if (!$?) { $exitCode = 1; echo "::error::Failed to minimize Log window!" } else { echo "::notice::Minimized Log window" }
exit $exitCode
Expand All @@ -119,18 +111,19 @@ jobs:
$exitCode = 0
& "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force background-hook.ahk "$PWD\bg-hook" 2>&1 | Out-Default
if (!$?) { $exitCode = 1; echo "::error::Test failed!" } else { echo "::notice::Test log" }
type bg-hook.log
$env:LARGE_FILES_DIRECTORY = "${env:RUNNER_TEMP}\large"
& "${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" }
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
341 changes: 341 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
272 changes: 272 additions & 0 deletions ui-tests/keystroke-order.ahk
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
#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'

; === cmd.exe input verification ===
; Verify that input typed into cmd.exe (a native Win32 console app) is not
; silently lost. This catches the regression where removing the pcon_start
; post-loop block also removed the pty_input_state = to_nat transition,
; causing keystrokes to go to the wrong pipe.
Info '=== cmd.exe input verification ==='
WinActivate(winId)
SetKeyDelay 20, 20
SendEvent('{Text}cmd.exe')
SendEvent('{Enter}')
; Type immediately without waiting for cmd.exe to fully start.
Sleep 200
SendEvent('{Text}echo ' testString)
SendEvent('{Enter}')

; Wait for the test string to appear in cmd.exe output.
deadline := A_TickCount + 10000
cmdOk := false
while A_TickCount < deadline
{
text := CaptureBufferFromMintty(winId)
; Look for the echoed string (cmd.exe prints the command AND its output)
; Count occurrences: the echo command line itself plus the output = at least 2
count := 0
searchPos := 1
while searchPos := InStr(text, testString, , searchPos)
{
count++
searchPos += StrLen(testString)
}
if count >= 2
{
Info 'cmd.exe echoed the test string correctly'
cmdOk := true
break
}
Sleep 500
}
if !cmdOk
{
Info 'Captured text:'
Info text
ExitWithError 'cmd.exe did not echo the test string (input lost?)'
}

; === Ctrl+H single-character delete verification ===
; When pseudo console is enabled, conhost.exe may translate Ctrl+H (0x08)
; into Ctrl+Backspace, which performs word-wise deletion instead of
; single-character deletion. Verify that Ctrl+H deletes only one char.
; See: https://inbox.sourceware.org/cygwin-patches/463c3df7-3810-ed9a-9f7c-c2cf4fd6a7b7@gmx.de/
Info '=== Ctrl+H single-character delete verification ==='
WinActivate(winId)
SetKeyDelay 20, 20
SendEvent('{Text}echo Expresso')
Sleep 200
Send '{Ctrl down}h{Ctrl up}'
Sleep 200
SendEvent('{Enter}')

deadline := A_TickCount + 10000
ctrlHOk := false
while A_TickCount < deadline
{
text := CaptureBufferFromMintty(winId)
; If Ctrl+H correctly deleted only 'o', the command executed was
; "echo Express" and cmd.exe printed "Express" as output. If Ctrl+H
; did a word-wise delete, "Expresso" was fully erased and cmd.exe
; ran "echo " which prints "ECHO is on." instead.
if InStr(text, 'Express') && !InStr(text, 'Expresso')
{
Info 'Ctrl+H correctly deleted only the last character'
ctrlHOk := true
break
}
Sleep 500
}
if !ctrlHOk
{
Info 'Captured text:'
Info text
ExitWithError 'Ctrl+H did not delete a single character (word-wise delete?)'
}

; Exit cmd.exe and verify we return to bash.
WinActivate(winId)
SetKeyDelay 20, 20
SendEvent('{Text}exit')
SendEvent('{Enter}')
Sleep 1000

text := CaptureBufferFromMintty(winId)
; After exiting cmd.exe we should see a bash prompt again.
; Find the last "$ " -- it should come after the cmd.exe session.
lastPrompt := 0
pos := 1
while pos := InStr(text, '$ ', , pos)
{
lastPrompt := pos
pos += 2
}
after := (lastPrompt > 0) ? Trim(SubStr(text, lastPrompt + 2)) : ''
if after != ''
{
Info 'WARNING: unexpected text after prompt: ' after
}
Info 'Back at bash prompt after cmd.exe'

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, 5)

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 + ",;" + BS*2 at a time.
SetKeyDelay 1, 1
Sleep 500
offset := 1
while offset <= StrLen(testString)
{
chunk := SubStr(testString, offset, chunkSize)
SendEvent('{Text}' chunk ',;')
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
}
Loading
Loading