Repository navigation
test(mcp): fire MCP client test deadlines on conditions instead of wall-clock time - #44166
Conversation
…ll-clock time Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…riggered deadline Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| def select(self, timeout: float | None = None) -> list[tuple[selectors.SelectorKey, int]]: | ||
| ready: Final = super().select(0) | ||
| if ready or timeout == 0: | ||
| return ready |
There was a problem hiding this comment.
Unit test uses clock This selector advances a manual clock, which the SSE test uses to drive its deadline. The
tests/unit guidance requires unit tests to use no clock, so this repository requirement must be satisfied before merging
Context Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
The guidance bans real wall-clock dependence. This loop is an injected virtual clock, the same fake _ManualClockLoop this file already uses, so no real time is read
There was a problem hiding this comment.
You're right. This does not introduce real wall-clock dependence: _AutojumpSelector only advances the injected _ManualClockLoop's virtual time when no I/O or callback is ready, and no wall-clock value is read. That is consistent with the existing fake-clock pattern in this file. My earlier comment was incorrect; I withdraw it.
…ot setup Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 03e4f67. Configure here.
TLDR
Problem this solves:
mcp-integrationshard in the last 24hHow it solves it:
User Flow
Before: a contributor's unrelated PR goes red on
mcp-integrationand they have to re-run itmcp-integration / Run testsjob fails ontest_outer_deadline_delivers_session_terminationwithassert started.is_set(), or ontest_sse_read_failure_is_preservedwithMCPError: Request 'tools/list' timed outAfter: the same commit gets a green
mcp-integrationjob on the first attemptFlake evidence (run date 2026-10-02)
tests/unit/experimental_mcp_client/test_mcp_client.py::test_outer_deadline_delivers_session_terminationFlaked 7 times in the window, always on the
mcp-integration / Run testsjob, attempt 1, on PRs that do not touch the MCP clientSame SHA flipped: https://github.com/BerriAI/litellm/actions/runs/36972046831 failed
[failure-True-True]on attempt 1 and passed on attempt 2 at3f066ee987Other failures, attempt 1 each: https://github.com/BerriAI/litellm/actions/runs/36874909254, https://github.com/BerriAI/litellm/actions/runs/36877132039, https://github.com/BerriAI/litellm/actions/runs/36895408264, https://github.com/BerriAI/litellm/actions/runs/36896660820 and https://github.com/BerriAI/litellm/actions/runs/36927552895 (all
[failure-True-True]), plus https://github.com/BerriAI/litellm/actions/runs/36895728486 ([ok-True-False])Mechanism: wall-clock dependence. The caller wrapped the tool call in a real
anyio.fail_after(0.2), and the fake server only setsstartedafter initialize and tools/list have both completed. In the failing CI run the test took 0.95s where its siblings took 0.2s, so the runner stalled the loop and the deadline fired during session setup, before the tool request was ever sent. Injecting a 0.25s synchronous stall into the fake server's initialize reply reproduces the sameassert started.is_set()failure on every parameter setFix: the deadline is still a real
fail_afterscope, but it starts with no limit and the fake server expires it the moment the slow tool call reaches it. Cancellation now always lands while the request is in flight, which is the case the test exists to cover. The 6.5s cleanup budget is now measured from the moment cancellation fires, so a slow setup no longer eats into it, and a separate 20s outer bound makes a regression where the tool call never reaches the server fail before the client's 30s timeouttests/unit/experimental_mcp_client/test_mcp_client.py::test_sse_read_failure_is_preservedFlaked 2 times in the window, attempt 1 each, on PRs that do not touch the MCP client: https://github.com/BerriAI/litellm/actions/runs/36959115506 and https://github.com/BerriAI/litellm/actions/runs/36967968247. It passed on every other
mcp-integrationrun in the window, including runs at neighboring main SHAsMechanism: wall-clock dependence. The client's 0.2s read timeout is real time, and it races delivery of the queued
ReadErrorthrough the SSE reader to the client's stream-error handler. The client only maps the timeout back to the original read error once that handler has run, so when a stalled loop lets the timer win, the rawRequest 'tools/list' timed outescapes. The CI log shows the handler running only during cleanup, after the timeout. Injecting a 0.25s stall right after the error is queued reproduces itFix: the test runs on a manual-clock event loop whose selector advances the clock to the next timer only when no callback or I/O is ready, the same idea as trio's autojump clock. The read timeout therefore fires only after the stream error has been fully delivered, regardless of runner speed. The 0.25s stall no longer affects it. The original 3s
wait_forstays as a virtual-time bound, so a regression that leaves the operation waiting forever fails instead of hanging the jobMutation checks
Skipping the transport exit that sends the session DELETE makes all 12 outer-deadline cases fail on
deleted. Lettingcall_toolswallow the cancellation makes theraise_on_error=Falsecases fail on the expectedTimeoutError. Disabling the client's stream-error mapping makes the SSE test fail with anMCPErrorinstead of the read error. Raising the client's session cleanup window from 5s to 8s makes everyhangcase fail on the cleanup budget. Removing the server's in-flight signal makes the outer-deadline test fail in 28s instead of 30s, and queueing no SSE error makes the SSE test fail in 7s instead of hanging. All mutations were revertedNot flakes in the window
Real breakages, failing on every attempt:
tests/proxy_migration_tests/test_prisma_toolchain.py::test_db_push_timeout_hint_names_the_per_command_budget(Lens data safety check refusesprisma db push), the two anthropic event-loop-stall tests intests/unit/proxy/pass_through_endpoints/test_streaming_handler.py, and the docs check flaggingLITELLM_DISABLE_LAZY_ROUTESas undocumented. Infra: one dependency install timed out at 8 minutes. None of these were touchedOther open deflake PRs
#43437, #42936 and #41219 each carry an approval, so they are left untouched under the human-reviewed exception and this PR is the new rolling PR
Pre-Submission checklist
uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Type
✅ Test
Caveats (if any)
Low
Final Attestation
Link to Devin session: https://app.devin.ai/sessions/26bdd1e8bf8149a48dfe897164ee8b33
Open in Devin Desktop: https://app.devin.ai/desktop/session/26bdd1e8bf8149a48dfe897164ee8b33?variant=devin
Note
Low Risk
Test-only changes to timing harnesses; no production MCP client behavior is modified.
Overview
Deflakes two MCP client unit tests that were racing real ~0.2s timers against mocked I/O on slow CI runners. Production code and assertions are unchanged.
Adds
_AutojumpSelector/_AutojumpClockLoop: a manual asyncio clock that advances only when the loop is idle (trio-style autojump).test_sse_read_failure_is_preservednow runs off pytest-asyncio on this loop so the client read timeout cannot beat delivery of the queuedReadError.test_outer_deadline_delivers_session_terminationno longer uses a fixedfail_after(0.2)from the start. The mock server sets the caller’sanyiodeadline once the slowtools/callis in flight, so cancellation always happens during the in-flight request. Cleanup timing is measured from that fire point, with a separate 20s outer bound if the server never signals._ManualClockLoopnow accepts an optional custom selector for the autojump wiring.Reviewed by Cursor Bugbot for commit 03e4f67. Bugbot is set up for automated code reviews on this repo. Configure here.