Skip to content

Commit 09b763f

Browse files
tablackburnclaude
andauthored
fix: replace TCP readiness check with signal file to fix flaky Linux test (#59)
* 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 <noreply@anthropic.com> (cherry picked from commit 85de79c) * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a083b14 commit 09b763f

1 file changed

Lines changed: 40 additions & 23 deletions

File tree

tests/Integration/Private/FileDownload.Integration.tests.ps1

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,20 @@ BeforeAll {
4343
return $port
4444
}
4545

46-
# Helper function to wait for server to be ready with polling
46+
# Helper function to wait for server to be ready by polling for a signal file
4747
function Wait-ServerReady {
4848
param(
49-
[int]$Port,
49+
[Parameter(Mandatory)]
50+
[ValidateNotNullOrEmpty()]
51+
[string]$SignalFile,
5052
[int]$TimeoutMs = 5000,
5153
[int]$PollIntervalMs = 50
5254
)
5355

5456
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
5557
while ($stopwatch.ElapsedMilliseconds -lt $TimeoutMs) {
56-
try {
57-
$client = [System.Net.Sockets.TcpClient]::new()
58-
$result = $client.BeginConnect('localhost', $Port, $null, $null)
59-
$success = $result.AsyncWaitHandle.WaitOne(100)
60-
if ($success) {
61-
$client.EndConnect($result)
62-
$client.Close()
63-
return $true
64-
}
65-
$client.Close()
66-
}
67-
catch {
68-
# Server not ready yet
58+
if (Test-Path -Path $SignalFile) {
59+
return $true
6960
}
7061
Start-Sleep -Milliseconds $PollIntervalMs
7162
}
@@ -82,8 +73,11 @@ BeforeAll {
8273
[switch]$CaptureHeaders
8374
)
8475

76+
$signalId = [guid]::NewGuid().ToString('N')
77+
$readySignalFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "pat-test-ready-$signalId.signal"
78+
8579
$job = Start-Job -ScriptBlock {
86-
param($port, $data, $statusCode, $captureHeaders)
80+
param($port, $data, $statusCode, $captureHeaders, $readySignalFile)
8781

8882
$result = @{
8983
ReceivedHeaders = @{}
@@ -96,8 +90,13 @@ BeforeAll {
9690
try {
9791
$listener.Start()
9892

99-
# Use async with timeout to prevent hanging
93+
# Begin async wait before signaling readiness so the
94+
# listener is in GetContextAsync() when the test fires
10095
$contextTask = $listener.GetContextAsync()
96+
97+
# Signal that the listener is ready to accept requests
98+
[System.IO.File]::WriteAllText($readySignalFile, 'ready')
99+
101100
if (-not $contextTask.Wait(30000)) {
102101
throw "Timeout waiting for request"
103102
}
@@ -132,12 +131,30 @@ BeforeAll {
132131
}
133132

134133
return $result
135-
} -ArgumentList $Port, $ResponseData, $StatusCode, $CaptureHeaders.IsPresent
136-
137-
# Wait for server to be ready with polling
138-
$ready = Wait-ServerReady -Port $Port -TimeoutMs 5000
139-
if (-not $ready) {
140-
Write-Warning "Server may not be ready on port $Port"
134+
} -ArgumentList $Port, $ResponseData, $StatusCode, $CaptureHeaders.IsPresent, $readySignalFile
135+
136+
# Wait for server to signal it is ready to accept HTTP requests. Wrap the
137+
# wait so a failed startup always stops the background job and removes the
138+
# readiness signal file instead of leaking them.
139+
try {
140+
$ready = Wait-ServerReady -SignalFile $readySignalFile -TimeoutMs 5000
141+
if (-not $ready) {
142+
throw "Server failed to become ready on port $Port within timeout"
143+
}
144+
}
145+
catch {
146+
$startupError = $_
147+
# Stop and remove the background job so a failed startup does not leak
148+
# the HttpListener and its 30-second GetContextAsync wait.
149+
$job | Stop-Job -ErrorAction 'SilentlyContinue'
150+
$job | Remove-Job -Force -ErrorAction 'SilentlyContinue'
151+
throw $startupError
152+
}
153+
finally {
154+
# Always remove the readiness signal file, even when readiness times out.
155+
if (Test-Path -Path $readySignalFile) {
156+
Remove-Item -Path $readySignalFile -Force
157+
}
141158
}
142159

143160
return $job

0 commit comments

Comments
 (0)