Skip to content

test(mcp): stop MCP dispatch tests from leaking real spawn watchdogs - #93

Closed
basil-k-aji-dev wants to merge 1 commit into
google:mainfrom
basil-k-aji-dev:test/mcp-stub-spawn-watchdog
Closed

basil-k-aji-dev wants to merge 1 commit into
google:mainfrom
basil-k-aji-dev:test/mcp-stub-spawn-watchdog

Conversation

@basil-k-aji-dev

Copy link
Copy Markdown

Closes #43.

Problem

mobile_run_task arms _start_spawn_watchdog after Popen returns. Four tests in test_mcp_tools.py mock Popen but leave the watchdog live, so each starts a real daemon thread monitoring a pid that came from a MagicMock.

Those threads outlive temp_trace_env's directory and its monkeypatches. Past the 60-second deadline they go on to terminate a process tree, cancel a reservation, write into the restored trace directory, and dispatch failure notifications — for tasks that never existed. The issue reports a real desktop "Artemis Task Failed" notification naming fake pid 54321.

Reproduction

I reproduced it with the guard the issue describes — a temporary conftest.py that intercepts threading.Thread.start for spawn-watchdog-* names, records the attempt without starting a thread, and asserts none occurred:

$ uv run pytest -q -p no:randomly tests/unit/mcp/test_mcp_tools.py
ERROR test_mobile_run_task_reserves_and_passes_global_queue_ticket
ERROR test_mobile_run_task_with_device_serial
ERROR test_mobile_run_task_forwards_pro_tuning_to_background_runner
ERROR test_mobile_run_task_omits_pro_tuning_flags_when_unset
AssertionError: Test leaked 1 real spawn watchdog thread(s)
25 passed, 4 errors in 0.50s

Same four tests, same counts as the issue.

The run also surfaced something not in the original report — the leaked watchdog reached the notifier and made an outbound HTTP request from a unit test:

urllib.error.URLError: <urlopen error [Errno 111] Connection refused>

so the blast radius includes network egress, not just local process and filesystem effects.

Change

A module-local autouse fixture records watchdog launches instead of starting them, and returns the recorded calls so tests can assert on them.

Rather than only suppressing the call, the queue-ticket and device-serial dispatch tests now assert its exact arguments:

assert stub_spawn_watchdog == [(result["trace_id"], 43210, "queue-ticket-1", "conv-1")]

so arming the watchdog goes from untested-but-harmful to actually covered.

The fixture is scoped to this module, not the suite. Production watchdog behaviour — startup, terminal tasks, deadlines, cleanup, notifications — stays covered by tests/unit/mcp/test_spawn_watchdog.py, so no coverage is lost.

Test-only; no production behaviour changes, as the issue notes none are needed.

Verification

Command Result
tests/unit/mcp/test_mcp_tools.py under the leak guard 25 passed (main: 25 passed, 4 errors)
tests/unit/mcp/test_mcp_tools.py normally 25 passed, no URLError
tests/unit/mcp/test_spawn_watchdog.py 4 passed
uv run pytest -q (full deterministic suite) 2096 passed, same 89 failing node ids as main — 0 new
uv run ruff check tests/ All checks passed

Both match the issue's "after" log (25 passed, 4 passed). The guard conftest.py was temporary and is not part of this PR.

make typecheck could not be run in my environment — the pyright wheel downloads a Node runtime on first use and my sandbox blocks it. The touched file is not in pyright-core.json.

`mobile_run_task` arms `_start_spawn_watchdog` after `Popen` returns. Four
tests in `test_mcp_tools.py` mock `Popen` but leave the watchdog live, so each
starts a real daemon thread monitoring a pid that came from a MagicMock.

Those threads outlive `temp_trace_env`'s directory and its monkeypatches. Past
the 60-second deadline they go on to terminate a process tree, cancel a
reservation, write into the restored trace directory and dispatch failure
notifications -- for tasks that never existed. The issue reports a real desktop
"Artemis Task Failed" notification naming fake pid 54321; reproducing it here
also produced an outbound HTTP call from the notifier
(`URLError: Connection refused`) during a unit-test run.

Add a module-local autouse fixture that records watchdog launches instead of
starting them, and assert the exact arguments in the queue-ticket and
device-serial dispatch tests, so the call is now covered rather than merely
suppressed.

Production watchdog behaviour stays covered by
`tests/unit/mcp/test_spawn_watchdog.py`. Test-only; no production change.

Closes google#43.
@basil-k-aji-dev

Copy link
Copy Markdown
Author

Closing as a duplicate. #42 by @wellorbetter already covers issue #43 and was opened first — that one should get the review, not this.

My mistake: I didn't check the open PR queue for an existing claim before sending this. Sorry for the noise.

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.

MCP unit tests leak real spawn watchdogs for mocked processes

1 participant