Skip to content

Commit a83d619

Browse files
Copilotericstj
andauthored
Centralize test timeout constants to fix CI flakiness (#1224)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com> Co-authored-by: Eric StJohn <ericstj@microsoft.com>
1 parent 283caf9 commit a83d619

7 files changed

Lines changed: 43 additions & 6 deletions

File tree

tests/Common/Utils/TestConstants.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,16 @@ public static class TestConstants
1010
/// Set to 60 seconds to provide sufficient buffer for slow CI environments.
1111
/// </summary>
1212
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60);
13+
14+
/// <summary>
15+
/// Timeout for HttpClient operations in tests.
16+
/// Set to 60 seconds to provide sufficient buffer for slow CI environments.
17+
/// </summary>
18+
public static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(60);
19+
20+
/// <summary>
21+
/// Timeout for short-lived HTTP requests during polling operations.
22+
/// Set to 2 seconds for quick failure detection while polling.
23+
/// </summary>
24+
public static readonly TimeSpan HttpClientPollingTimeout = TimeSpan.FromSeconds(2);
1325
}

tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ public async ValueTask DisposeAsync()
8383

8484
protected async Task<WebApplication> StartMcpServerAsync(string path = "", string? authScheme = null)
8585
{
86+
// Wait for the OAuth server to be ready before starting the MCP server.
87+
// This prevents race conditions in CI where the OAuth server may not be
88+
// fully initialized when the first test request is made.
89+
await TestOAuthServer.ServerStarted.WaitAsync(TestContext.Current.CancellationToken);
90+
8691
Builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
8792
{
8893
options.TokenValidationParameters.ValidAudience = $"{McpServerUrl}{path}";

tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public async ValueTask InitializeAsync()
4747
// Wait for server to be ready (retry for up to 30 seconds)
4848
var timeout = TimeSpan.FromSeconds(30);
4949
var stopwatch = Stopwatch.StartNew();
50-
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
50+
using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientPollingTimeout };
5151

5252
while (stopwatch.Elapsed < timeout)
5353
{

tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public KestrelInMemoryTest(ITestOutputHelper testOutputHelper)
4242
protected static void ConfigureHttpClient(HttpClient httpClient)
4343
{
4444
httpClient.BaseAddress = new Uri("http://localhost:5000/");
45-
httpClient.Timeout = TimeSpan.FromSeconds(10);
45+
httpClient.Timeout = TestConstants.HttpClientTimeout;
4646
}
4747

4848
public override void Dispose()

tests/ModelContextProtocol.TestOAuthServer/Program.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public sealed class Program
3333

3434
private readonly ILoggerProvider? _loggerProvider;
3535
private readonly IConnectionListenerFactory? _kestrelTransport;
36+
private readonly TaskCompletionSource _serverStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);
3637

3738
/// <summary>
3839
/// Initializes a new instance of the <see cref="Program"/> class with logging and transport parameters.
@@ -47,6 +48,11 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor
4748
_kestrelTransport = kestrelTransport;
4849
}
4950

51+
/// <summary>
52+
/// Gets a task that completes when the server has started and is ready to accept connections.
53+
/// </summary>
54+
public Task ServerStarted => _serverStarted.Task;
55+
5056
// Track if we've already issued an already-expired token for the CanAuthenticate_WithTokenRefresh test which uses the test-refresh-client registration.
5157
public bool HasRefreshedToken { get; set; }
5258

@@ -541,7 +547,20 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null)
541547
Console.WriteLine($"Demo Client ID: {clientId}");
542548
Console.WriteLine($"Demo Client Secret: {clientSecret}");
543549

544-
await app.RunAsync(cancellationToken);
550+
await app.StartAsync(cancellationToken);
551+
_serverStarted.TrySetResult();
552+
553+
// Wait until cancellation is requested
554+
try
555+
{
556+
await Task.Delay(Timeout.Infinite, cancellationToken);
557+
}
558+
catch (OperationCanceledException)
559+
{
560+
// Expected when cancellation is requested
561+
}
562+
563+
await app.StopAsync();
545564
}
546565

547566
/// <summary>

tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Diagnostics;
22
using System.Net;
3+
using ModelContextProtocol.Tests.Utils;
34

45
namespace ModelContextProtocol.Tests;
56

@@ -33,7 +34,7 @@ public async Task StartAsync()
3334
?? throw new InvalidOperationException($"Could not start process for {processStartInfo.FileName} with '{processStartInfo.Arguments}'.");
3435

3536
// Poll until the server is ready (up to 30 seconds)
36-
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
37+
using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientPollingTimeout };
3738
var endpoint = $"http://localhost:{_port}/sse";
3839
var deadline = DateTime.UtcNow.AddSeconds(30);
3940

@@ -72,7 +73,7 @@ public async ValueTask DisposeAsync()
7273

7374
using var stopProcess = Process.Start(stopInfo)
7475
?? throw new InvalidOperationException($"Could not stop process for {stopInfo.FileName} with '{stopInfo.Arguments}'.");
75-
await stopProcess.WaitForExitAsync(TimeSpan.FromSeconds(10));
76+
await stopProcess.WaitForExitAsync(TestConstants.DefaultTimeout);
7677
}
7778
catch (Exception ex)
7879
{

tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public async Task SigInt_DisposesTestServerWithHosting_Gracefully()
4646
// https://github.com/dotnet/runtime/issues/109432, https://github.com/dotnet/runtime/issues/44944
4747
Assert.Equal(0, kill(process.Id, SIGINT));
4848

49-
await process.WaitForExitAsync(TimeSpan.FromSeconds(10));
49+
await process.WaitForExitAsync(TestConstants.DefaultTimeout);
5050

5151
Assert.True(process.HasExited);
5252
Assert.Equal(0, process.ExitCode);

0 commit comments

Comments
 (0)