test: deflake timing-dependent tests (#335)

* test: deflake timing-dependent tests

Inject a clock into channel dedup tests, replace fixed async sleeps with
events/explicit flushes, and avoid wall-clock waits in background tests.

* coderabbit nit
This commit is contained in:
dinos
2026-07-07 09:25:35 +02:00
committed by GitHub
parent df54d8498c
commit be3dd272c3
4 changed files with 138 additions and 65 deletions
+53 -37
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
import threading
import pytest
@@ -93,6 +94,32 @@ def _make_loader_fn(agent_value="AGENT", fail_with=None, capture=None):
return _loader
class _GatedThreadLoader:
"""Callable loader that blocks until tests explicitly release it."""
def __init__(self, agent_value="AGENT", progress_events=()):
self.agent_value = agent_value
self.progress_events = tuple(progress_events)
self.started = threading.Event()
self.release = threading.Event()
self.finished = threading.Event()
def __call__(self, *, on_mcp_progress=None):
self.started.set()
self.release.wait(timeout=1)
try:
if on_mcp_progress is not None:
for event in self.progress_events:
on_mcp_progress(*event)
return self.agent_value
finally:
self.finished.set()
async def _wait_for_event(event, timeout=1):
return await asyncio.to_thread(event.wait, timeout)
def _run(coro):
return asyncio.run(coro)
@@ -125,23 +152,24 @@ class TestBackgroundAgentLoaderStart:
_run(_go())
def test_start_cancels_in_flight_prior_task(self):
import time
def _blocking(*, on_mcp_progress=None):
time.sleep(0.05)
return "LATE"
blocking = _GatedThreadLoader("LATE")
async def _go():
loader = BackgroundAgentLoader(_blocking)
loader = BackgroundAgentLoader(blocking)
loader.start()
first_task = loader.task
assert first_task is not None
assert await _wait_for_event(blocking.started)
# Supersede immediately; asyncio.to_thread wrapper gets cancelled.
loader._loader_fn = _make_loader_fn("FRESH")
loader.start()
agent = await loader.await_ready()
assert agent == "FRESH"
# Let the first thread drain so its done callback (gated) fires.
await asyncio.sleep(0.1)
blocking.release.set()
try:
await first_task
except asyncio.CancelledError:
pass
assert first_task.cancelled() or first_task.done()
_run(_go())
@@ -164,17 +192,11 @@ class TestBackgroundAgentLoaderCallbacks:
def test_stale_progress_events_are_dropped(self):
"""A progress event fired after a newer `start` must not reach the hook."""
import time
slow_loader = _GatedThreadLoader(
"slow-agent", progress_events=[("success", "from-slow", "1")]
)
seen: list[str] = []
# Loader 1 sleeps so its progress event fires AFTER load 2 starts.
def slow_loader(*, on_mcp_progress=None):
time.sleep(0.08)
if on_mcp_progress is not None:
on_mcp_progress("success", "from-slow", "1")
return "slow-agent"
def fast_loader(*, on_mcp_progress=None):
if on_mcp_progress is not None:
on_mcp_progress("success", "from-fast", "1")
@@ -186,13 +208,13 @@ class TestBackgroundAgentLoaderCallbacks:
async def _go():
loader.start()
# Supersede before the slow thread's event fires.
await asyncio.sleep(0.01)
assert await _wait_for_event(slow_loader.started)
# Loader 1 waits so its progress event fires AFTER load 2 starts.
loader._loader_fn = fast_loader
loader.start()
await loader.await_ready()
# Let the superseded thread finish (its event is gated out).
await asyncio.sleep(0.1)
slow_loader.release.set()
assert await _wait_for_event(slow_loader.finished)
_run(_go())
assert "from-fast" in seen
@@ -307,21 +329,17 @@ class TestBackgroundAgentLoaderAdopt:
def test_adopt_supersedes_in_flight_load(self):
"""A late background completion must not overwrite an adopted agent."""
import time
slow_loader = _GatedThreadLoader("FROM_BACKGROUND")
def _slow(*, on_mcp_progress=None):
time.sleep(0.08)
return "FROM_BACKGROUND"
loader = BackgroundAgentLoader(_slow)
loader = BackgroundAgentLoader(slow_loader)
async def _go():
loader.start()
await asyncio.sleep(0.01)
assert await _wait_for_event(slow_loader.started)
loader.adopt("FROM_MODEL")
# Give the background thread time to finish and fire its
# done-callback; the generation token should make it a no-op.
await asyncio.sleep(0.1)
slow_loader.release.set()
assert await _wait_for_event(slow_loader.finished)
await asyncio.sleep(0)
assert loader.agent == "FROM_MODEL"
_run(_go())
@@ -343,17 +361,15 @@ class TestBackgroundAgentLoaderIsPending:
assert not loader.is_pending
def test_true_between_start_and_completion(self):
import time
wait_loader = _GatedThreadLoader("ok")
def _wait_loader(*, on_mcp_progress=None):
time.sleep(0.05)
return "ok"
loader = BackgroundAgentLoader(_wait_loader)
loader = BackgroundAgentLoader(wait_loader)
async def _go():
loader.start()
assert await _wait_for_event(wait_loader.started)
assert loader.is_pending
wait_loader.release.set()
await loader.await_ready()
assert not loader.is_pending