Skip to content

Harden MCP OAuth cancel E2E tests against create/interest race#2029

Merged
stephentoub merged 2 commits into
mainfrom
stephentoub-investigate-csharp-ci-race
Jul 18, 2026
Merged

Harden MCP OAuth cancel E2E tests against create/interest race#2029
stephentoub merged 2 commits into
mainfrom
stephentoub-investigate-csharp-ci-race

Conversation

@stephentoub

Copy link
Copy Markdown
Collaborator

Why

The C# SDK CI leg in copilot-agent-runtime has been intermittently failing on McpOAuthE2ETests.Should_Cancel_Pending_MCP_OAuth_Request with observedRequest being null. Investigation traced this to a race, not a runtime bug: the runtime behaves exactly per its documented contract.

session.create kicks off the MCP server connection, but the SDK only registers its mcp.oauth_required event interest after create returns. When the OAuth server's initial 401 wins that race, the runtime records the server as needs-auth without invoking the host callback (its documented "no listener" path). The cancel tests then sampled the host callback the instant needs-auth was observed, so observedRequest was 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-auth first appears. Once interest is registered, the runtime's auth retry re-drives the OAuth flow and invokes the host callback with the same initial reason, so all existing assertions stay valid. The cancel path is still genuinely exercised.

Applied uniformly across all five SDK suites that share this pattern:

  • C# - TestHelper.WaitForConditionAsync(...) before the asserts
  • Go - poll loop plus a sync.Mutex guarding the observed request (also fixes a latent data race between the callback goroutine and the test)
  • Python - await wait_for_condition(...) inside the session scope
  • Java - reuse the existing waitForAuthRequest(...) helper, moved inside the session scope
  • Rust - wait_for_condition(...) on handler.request

Notes for reviewers

  • This is a test-robustness fix only; no product/runtime or SDK source code changes.
  • The reason == initial assertion is preserved on purpose: I confirmed in the runtime source that both the initial connect and the later auth retry emit reason: "initial" (retry only becomes reauth when forceReauth is set, which it isn't here).
  • Empirical corroboration: Should_Satisfy (which waits for Connected) 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.
  • Validated locally: C# build, go vet, Python py_compile, Java test-compile + spotless:check, and Rust cargo check --tests all 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

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
@stephentoub
stephentoub requested a review from a team as a code owner July 18, 2026 03:01
Copilot AI review requested due to automatic review settings July 18, 2026 03:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread dotnet/test/E2E/McpOAuthE2ETests.cs Outdated
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
@github-actions

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review

No consistency issues found.

This PR only modifies dotnet/test/E2E/McpOAuthE2ETests.cs, a .NET-specific E2E test file. The change replaces a polling-based wait with a TaskCompletionSource pattern for thread-safe callback handling — an internal test implementation detail specific to C# async patterns.

No SDK API surfaces, public types, or user-observable behavior were changed. No cross-language updates are needed.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

Generated by SDK Consistency Review Agent for #2029 · 14.2 AIC · ⌖ 7.49 AIC · ⊞ 5K ·

@stephentoub
stephentoub merged commit 3cbf4e7 into main Jul 18, 2026
51 checks passed
@stephentoub
stephentoub deleted the stephentoub-investigate-csharp-ci-race branch July 18, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants