Harden MCP OAuth cancel E2E tests against create/interest race#2029
Conversation
The MCP OAuth "cancel" E2E tests sampled the host auth callback the instant the server reached `needs-auth`, which is racy: `session.create` kicks off the MCP connection, but the SDK only registers its `mcp.oauth_required` event interest after create returns. When the server's initial 401 wins that race, the runtime records `needs-auth` without invoking the host callback, so the callback observation was intermittently empty (e.g. the flaky C# CI leg in copilot-agent-runtime). Wait for the callback to be invoked (bounded, reusing each suite's existing wait-for-condition helper) instead of sampling it immediately. A later runtime auth retry fires the callback with the same `initial` reason, so the assertions stay valid. Applied uniformly to C#, Go, Python, Java, and Rust; the Go change also guards the observed request with a mutex to fix a latent data race. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7849882-a2a2-4587-a602-da7718889a8c
There was a problem hiding this comment.
Pull request overview
Hardens MCP OAuth cancellation E2E tests against callback-registration races across five SDKs.
Changes:
- Waits for the OAuth callback before assertions.
- Adds synchronized request tracking in Go.
- Keeps waits within active session scopes.
Show a summary per file
| File | Description |
|---|---|
rust/tests/e2e/mcp_oauth.rs |
Waits for the handler request. |
python/e2e/test_mcp_oauth_e2e.py |
Polls for the observed request. |
java/src/test/java/com/github/copilot/McpOAuthE2ETest.java |
Reuses the auth-request wait helper. |
go/internal/e2e/mcp_oauth_e2e_test.go |
Adds synchronized polling. |
dotnet/test/E2E/McpOAuthE2ETests.cs |
Adds callback polling, but retains an unsynchronized cross-thread read. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Medium
Addresses review feedback: the previous poll read observedRequest (written by the callback thread) from the test continuation without synchronization. Switch to the TaskCompletionSource pattern already used by the direct-RPC test in this file, so the callback result is handed off safely and awaited with a timeout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7849882-a2a2-4587-a602-da7718889a8c
Cross-SDK Consistency Review✅ No consistency issues found. This PR only modifies No SDK API surfaces, public types, or user-observable behavior were changed. No cross-language updates are needed. Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
Why
The C# SDK CI leg in
copilot-agent-runtimehas been intermittently failing onMcpOAuthE2ETests.Should_Cancel_Pending_MCP_OAuth_RequestwithobservedRequestbeing null. Investigation traced this to a race, not a runtime bug: the runtime behaves exactly per its documented contract.session.createkicks off the MCP server connection, but the SDK only registers itsmcp.oauth_requiredevent interest after create returns. When the OAuth server's initial401wins that race, the runtime records the server asneeds-authwithout invoking the host callback (its documented "no listener" path). The cancel tests then sampled the host callback the instantneeds-authwas observed, soobservedRequestwas intermittently empty.Approach
Wait for the callback to actually be invoked (bounded, reusing each suite's existing wait-for-condition helper) instead of sampling it the moment
needs-authfirst appears. Once interest is registered, the runtime's auth retry re-drives the OAuth flow and invokes the host callback with the sameinitialreason, so all existing assertions stay valid. The cancel path is still genuinely exercised.Applied uniformly across all five SDK suites that share this pattern:
TestHelper.WaitForConditionAsync(...)before the assertssync.Mutexguarding the observed request (also fixes a latent data race between the callback goroutine and the test)await wait_for_condition(...)inside the session scopewaitForAuthRequest(...)helper, moved inside the session scopewait_for_condition(...)onhandler.requestNotes for reviewers
reason == initialassertion is preserved on purpose: I confirmed in the runtime source that both the initial connect and the later auth retry emitreason: "initial"(retry only becomesreauthwhenforceReauthis set, which it isn't here).Should_Satisfy(which waits forConnected) has stayed green while only the cancel test's instantaneous assert was flaky, which shows the retry reliably invokes the callback within the test window.go vet, Pythonpy_compile, Javatest-compile+spotless:check, and Rustcargo check --testsall pass. The full E2E suites need a live CLI, a local OAuth server, and the replay proxy, and the rare race cannot be reproduced deterministically, so they were not run end to end.Generated by Copilot