test: harden timing-fragile test and add cross-tool ContextVar isolation test

Replace asyncio.sleep(0.05) with an asyncio.Event + patched Lock.acquire
to guarantee the waiting task has reached the lock before asserting.  Add
a test confirming LongTaskTool and CompleteGoalTool ContextVars are
isolated, and document the design intent in _GoalToolsMixin.
This commit is contained in:
chengyongru 2026-05-28 22:36:26 +08:00 committed by Xubin Ren
parent 0df60416ba
commit 84428136e6
3 changed files with 34 additions and 4 deletions

View File

@ -46,6 +46,9 @@ class _GoalToolsMixin(ContextAware):
def __init__(self, sessions: SessionManager, bus: Any | None = None) -> None:
self._sessions = sessions
self._bus = bus
# Each subclass gets its own ContextVar so concurrent tasks across
# different tool types (LongTaskTool vs CompleteGoalTool) do not
# interfere with each other.
self._request_ctx: ContextVar[RequestContext | None] = ContextVar(
f"{self.__class__.__name__}_request_ctx",
default=None,

View File

@ -566,10 +566,21 @@ async def test_waiting_dispatch_does_not_replace_active_pending_queue(tmp_path):
active_pending = asyncio.Queue(maxsize=1)
loop._pending_queues[session_key] = active_pending
waiting = asyncio.create_task(
loop._dispatch(InboundMessage(channel="cli", sender_id="u", chat_id="c", content="queued"))
)
await asyncio.sleep(0.05)
waiting_at_lock = asyncio.Event()
original_acquire = asyncio.Lock.acquire
async def _patched_acquire(self, *args, **kwargs):
if self is lock:
waiting_at_lock.set()
return await original_acquire(self, *args, **kwargs)
with patch.object(asyncio.Lock, "acquire", _patched_acquire):
waiting = asyncio.create_task(
loop._dispatch(
InboundMessage(channel="cli", sender_id="u", chat_id="c", content="queued")
)
)
await asyncio.wait_for(waiting_at_lock.wait(), timeout=2.0)
assert loop._pending_queues[session_key] is active_pending

View File

@ -100,6 +100,22 @@ async def test_goal_tools_keep_request_context_per_task(tmp_path):
assert sm.get_or_create("websocket:b").metadata[GOAL_STATE_KEY]["recap"] == "Done B"
@pytest.mark.asyncio
async def test_goal_tools_context_isolated_across_tool_types(tmp_path):
"""LongTaskTool and CompleteGoalTool must not share routing context."""
sm = SessionManager(tmp_path)
lt = LongTaskTool(sessions=sm)
cg = CompleteGoalTool(sessions=sm)
ctx = RequestContext(channel="websocket", chat_id="a", session_key="websocket:a")
lt.set_context(ctx)
assert cg._request_ctx.get() is None
cg.set_context(ctx)
assert lt._request_ctx.get() is ctx
assert cg._request_ctx.get() is ctx
@pytest.mark.asyncio
async def test_long_task_publishes_goal_state_ws_after_save(tmp_path):
bus = MagicMock()