From e4a2ea076802544671e3fc5177ed039dab66e7c4 Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Tue, 24 Feb 2026 20:48:06 -0500 Subject: [PATCH 1/2] fix: Replace TCP readiness check with signal file to fix flaky Linux test Wait-ServerReady used a TCP socket connect check that succeeded as soon as HttpListener.Start() opened the port. On Linux, the background job had not yet called GetContextAsync(), so the test HTTP request arrived before the listener was ready, causing intermittent failures. Replace with a signal file approach: the background job writes a temp file after entering GetContextAsync(), and the parent polls for that file. This is deterministic and eliminates the race condition. Co-Authored-By: Claude Opus 4.6 (cherry picked from commit 85de79cc1ba57728a7af2379058fadfe43a707bb) --- .../FileDownload.Integration.tests.ps1 | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/tests/Integration/Private/FileDownload.Integration.tests.ps1 b/tests/Integration/Private/FileDownload.Integration.tests.ps1 index 6a774fa..7abdec2 100644 --- a/tests/Integration/Private/FileDownload.Integration.tests.ps1 +++ b/tests/Integration/Private/FileDownload.Integration.tests.ps1 @@ -43,29 +43,19 @@ BeforeAll { return $port } - # Helper function to wait for server to be ready with polling + # Helper function to wait for server to be ready by polling for a signal file function Wait-ServerReady { param( - [int]$Port, + [Parameter(Mandatory)] + [string]$SignalFile, [int]$TimeoutMs = 5000, [int]$PollIntervalMs = 50 ) $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() while ($stopwatch.ElapsedMilliseconds -lt $TimeoutMs) { - try { - $client = [System.Net.Sockets.TcpClient]::new() - $result = $client.BeginConnect('localhost', $Port, $null, $null) - $success = $result.AsyncWaitHandle.WaitOne(100) - if ($success) { - $client.EndConnect($result) - $client.Close() - return $true - } - $client.Close() - } - catch { - # Server not ready yet + if (Test-Path -Path $SignalFile) { + return $true } Start-Sleep -Milliseconds $PollIntervalMs } @@ -82,8 +72,11 @@ BeforeAll { [switch]$CaptureHeaders ) + $signalId = [guid]::NewGuid().ToString('N') + $readySignalFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "pat-test-ready-$signalId.signal" + $job = Start-Job -ScriptBlock { - param($port, $data, $statusCode, $captureHeaders) + param($port, $data, $statusCode, $captureHeaders, $readySignalFile) $result = @{ ReceivedHeaders = @{} @@ -96,8 +89,13 @@ BeforeAll { try { $listener.Start() - # Use async with timeout to prevent hanging + # Begin async wait before signaling readiness so the + # listener is in GetContextAsync() when the test fires $contextTask = $listener.GetContextAsync() + + # Signal that the listener is ready to accept requests + [System.IO.File]::WriteAllText($readySignalFile, 'ready') + if (-not $contextTask.Wait(30000)) { throw "Timeout waiting for request" } @@ -132,12 +130,17 @@ BeforeAll { } return $result - } -ArgumentList $Port, $ResponseData, $StatusCode, $CaptureHeaders.IsPresent + } -ArgumentList $Port, $ResponseData, $StatusCode, $CaptureHeaders.IsPresent, $readySignalFile - # Wait for server to be ready with polling - $ready = Wait-ServerReady -Port $Port -TimeoutMs 5000 + # Wait for server to signal it is ready to accept HTTP requests + $ready = Wait-ServerReady -SignalFile $readySignalFile -TimeoutMs 5000 if (-not $ready) { - Write-Warning "Server may not be ready on port $Port" + throw "Server failed to become ready on port $Port within timeout" + } + + # Clean up signal file + if (Test-Path -Path $readySignalFile) { + Remove-Item -Path $readySignalFile -Force } return $job From 52dd7bbe4529c9091bb099b7cd9e8d044b2771ea Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Mon, 25 May 2026 11:26:14 -0400 Subject: [PATCH 2/2] fix: clean up test server job and signal file on readiness failure Wrap the readiness wait in Start-TestHttpServerJob with try/catch/finally so a startup that times out no longer leaks resources: - The catch stops and removes the background job, releasing the HttpListener and its 30-second GetContextAsync wait instead of orphaning the job. - The finally always removes the readiness signal file, even when the wait times out and throws. - Add ValidateNotNullOrEmpty to the Wait-ServerReady SignalFile parameter. Addresses review feedback on the integration test scaffolding. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../FileDownload.Integration.tests.ps1 | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/Integration/Private/FileDownload.Integration.tests.ps1 b/tests/Integration/Private/FileDownload.Integration.tests.ps1 index 7abdec2..c3d62da 100644 --- a/tests/Integration/Private/FileDownload.Integration.tests.ps1 +++ b/tests/Integration/Private/FileDownload.Integration.tests.ps1 @@ -47,6 +47,7 @@ BeforeAll { function Wait-ServerReady { param( [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] [string]$SignalFile, [int]$TimeoutMs = 5000, [int]$PollIntervalMs = 50 @@ -132,15 +133,28 @@ BeforeAll { return $result } -ArgumentList $Port, $ResponseData, $StatusCode, $CaptureHeaders.IsPresent, $readySignalFile - # Wait for server to signal it is ready to accept HTTP requests - $ready = Wait-ServerReady -SignalFile $readySignalFile -TimeoutMs 5000 - if (-not $ready) { - throw "Server failed to become ready on port $Port within timeout" + # Wait for server to signal it is ready to accept HTTP requests. Wrap the + # wait so a failed startup always stops the background job and removes the + # readiness signal file instead of leaking them. + try { + $ready = Wait-ServerReady -SignalFile $readySignalFile -TimeoutMs 5000 + if (-not $ready) { + throw "Server failed to become ready on port $Port within timeout" + } } - - # Clean up signal file - if (Test-Path -Path $readySignalFile) { - Remove-Item -Path $readySignalFile -Force + catch { + $startupError = $_ + # Stop and remove the background job so a failed startup does not leak + # the HttpListener and its 30-second GetContextAsync wait. + $job | Stop-Job -ErrorAction 'SilentlyContinue' + $job | Remove-Job -Force -ErrorAction 'SilentlyContinue' + throw $startupError + } + finally { + # Always remove the readiness signal file, even when readiness times out. + if (Test-Path -Path $readySignalFile) { + Remove-Item -Path $readySignalFile -Force + } } return $job