From a103d755ced41a34c4cbc6878c8ae33eccffb72d Mon Sep 17 00:00:00 2001 From: drew Date: Tue, 16 Jun 2026 22:33:14 -0400 Subject: [PATCH] test(mcp): cover timer shutdown scheduling guards --- .../tdd_mcp_client_timer_cancel_race_steps.py | 44 +++++++++++++++++++ .../tdd_mcp_client_timer_cancel_race.feature | 8 +++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/features/steps/tdd_mcp_client_timer_cancel_race_steps.py b/features/steps/tdd_mcp_client_timer_cancel_race_steps.py index 5ff0c078d..c78f45185 100644 --- a/features/steps/tdd_mcp_client_timer_cancel_race_steps.py +++ b/features/steps/tdd_mcp_client_timer_cancel_race_steps.py @@ -88,6 +88,35 @@ def step_shared_callback_flag(context: Context) -> None: context.timer_race_callback_lock = threading.Lock() +@given("an McpClient with active idle and health timers") +def step_mcp_client_active_timers(context: Context) -> None: + """Create a client whose timer schedulers would normally install timers.""" + server_config = MCPServerConfig( + name="test-timer-shutdown-guard", + transport="stdio", + command="echo", + ) + context.timer_race_config = McpClientConfig( + server=server_config, + lazy_start=False, + idle_timeout_seconds=60.0, + health_check_interval_seconds=60.0, + ) + context.timer_race_transport = MockMCPTransport( + tools=[_mock_tool("test_tool")], + ) + context.timer_race_client = McpClient( + config=context.timer_race_config, + transport=context.timer_race_transport, + ) + + +@given("the timer race client is marked as shutting down") +def step_mark_timer_race_client_shutting_down(context: Context) -> None: + """Simulate the shutdown window before timer scheduling is attempted.""" + context.timer_race_client._shutting_down = True + + # ── When ────────────────────────────────────────────────────────── @@ -213,6 +242,13 @@ def step_wait_for_late_fires(context: Context) -> None: original_sleep(0.3) +@when("I ask the client to schedule idle and health timers") +def step_schedule_idle_and_health_timers(context: Context) -> None: + """Invoke both scheduler guards while shutdown is in progress.""" + context.timer_race_client._schedule_idle_timer() + context.timer_race_client._schedule_health_check() + + # ── Then ────────────────────────────────────────────────────────── @@ -237,3 +273,11 @@ def step_idle_callback_not_fired_after_shutdown(context: Context) -> None: "different synchronisation scheme to prevent the race. " f"Errors (if any): {getattr(context, 'timer_race_errors', [])}" ) + + +@then("no idle or health timer should be scheduled") +def step_no_idle_or_health_timer_scheduled(context: Context) -> None: + """Assert shutdown guards returned before installing timers.""" + client = context.timer_race_client + assert client._idle_timer is None, "Idle timer was scheduled during shutdown" + assert client._health_timer is None, "Health timer was scheduled during shutdown" diff --git a/features/tdd_mcp_client_timer_cancel_race.feature b/features/tdd_mcp_client_timer_cancel_race.feature index 54c739789..63ac4261e 100644 --- a/features/tdd_mcp_client_timer_cancel_race.feature +++ b/features/tdd_mcp_client_timer_cancel_race.feature @@ -75,4 +75,10 @@ Feature: TDD Issue #10516 — McpClient timer can fire after cancellation race When I start the client And I trigger the idle timer race by forcing a reschedule during shutdown And I wait long enough for any late timer fires (0.3s) - Then the idle timer callback should not have fired after shutdown \ No newline at end of file + Then the idle timer callback should not have fired after shutdown + + Scenario: Timer schedulers skip work during shutdown + Given an McpClient with active idle and health timers + And the timer race client is marked as shutting down + When I ask the client to schedule idle and health timers + Then no idle or health timer should be scheduled