fix: resolve flaky test in CI by adding proper shutdown sequence#25
Conversation
Agent-Logs-Url: https://github.com/teh-hippo/tiny-ntfy-mcp/sessions/5a3ae47b-b9c3-46a7-bfee-44a450d0942f Co-authored-by: teh-hippo <490309+teh-hippo@users.noreply.github.com>
The test_stdio_initialize_and_tools_list test was flaky due to a race condition where the MCP server would see EOF on stdin and shut down before flushing all responses to stdout. This caused the tools/list response to be lost intermittently (~20-30% failure rate). The fix adds a proper shutdown sequence (shutdown request + exit notification) before closing stdin, ensuring the server processes and flushes all pending responses before terminating. Tested with 90 consecutive runs - all passed (previously ~30% failed). Agent-Logs-Url: https://github.com/teh-hippo/tiny-ntfy-mcp/sessions/5a3ae47b-b9c3-46a7-bfee-44a450d0942f Co-authored-by: teh-hippo <490309+teh-hippo@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Resolves a flaky CI test by ensuring the MCP server receives an explicit shutdown sequence so it flushes pending responses before exiting.
Changes:
- Extend
test_stdio_initialize_and_tools_listto sendshutdownandnotifications/exitaftertools/list. - Switch the test runner from
subprocess.run(...)tosubprocess.Popen(...).communicate(...)and improve assertion error messages. - Update
uv.lockto reflecttiny-ntfy-mcpversion2.0.9.
Reviewed changes
Copilot reviewed 1 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
tests/test_mcp_server.py |
Adds shutdown/exit messages, changes subprocess invocation, and improves missing-response diagnostics. |
uv.lock |
Updates locked editable package version to 2.0.9. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| check=False, | ||
| ) | ||
| assert proc.returncode == 0, proc.stderr | ||
| stdout, stderr = proc.communicate(input=input_text, timeout=5) |
There was a problem hiding this comment.
proc.communicate(..., timeout=5) can raise subprocess.TimeoutExpired; unlike subprocess.run(timeout=...), this won’t automatically terminate the child process. If the timeout is hit (e.g., CI slowdown), the test can leak a running MCP server process and potentially impact subsequent tests. Wrap communicate() in a try/except (kill/terminate + second communicate/wait in finally) or switch back to subprocess.run(..., timeout=...) now that the shutdown/exit sequence is included.
| stdout, stderr = proc.communicate(input=input_text, timeout=5) | |
| try: | |
| stdout, stderr = proc.communicate(input=input_text, timeout=5) | |
| except subprocess.TimeoutExpired: | |
| proc.kill() | |
| stdout, stderr = proc.communicate() | |
| pytest.fail(f"tiny_ntfy_mcp did not exit within timeout; stdout={stdout!r}, stderr={stderr!r}") | |
| finally: | |
| if proc.poll() is None: | |
| proc.kill() | |
| proc.wait() |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| assert 2 in responses_by_id, f"Missing tools/list response (id=2). Got responses: {[r.get('id') for r in responses]}" | ||
|
|
||
| init_resp = responses_by_id[1] | ||
| tools_resp = responses_by_id[2] | ||
|
|
||
| assert init_resp["result"]["serverInfo"]["name"] == "tiny-ntfy-mcp" | ||
| assert init_resp["result"]["serverInfo"]["version"] |
There was a problem hiding this comment.
This test now sends a shutdown request (id=3) but never asserts that the corresponding response was received. Adding an assertion for id=3 would ensure the graceful shutdown path actually ran (and would make failures easier to diagnose if shutdown handling regresses).
| assert 2 in responses_by_id, f"Missing tools/list response (id=2). Got responses: {[r.get('id') for r in responses]}" | |
| init_resp = responses_by_id[1] | |
| tools_resp = responses_by_id[2] | |
| assert init_resp["result"]["serverInfo"]["name"] == "tiny-ntfy-mcp" | |
| assert init_resp["result"]["serverInfo"]["version"] | |
| assert 2 in responses_by_id, f"Missing tools/list response (id=2). Got responses: {[r.get('id') for r in responses]}" | |
| assert 3 in responses_by_id, f"Missing shutdown response (id=3). Got responses: {[r.get('id') for r in responses]}" | |
| init_resp = responses_by_id[1] | |
| tools_resp = responses_by_id[2] | |
| shutdown_resp = responses_by_id[3] | |
| assert init_resp["result"]["serverInfo"]["name"] == "tiny-ntfy-mcp" | |
| assert init_resp["result"]["serverInfo"]["version"] | |
| assert "result" in shutdown_resp |
|
Superseded by #28, which uses a more robust approach that also addresses the |
Summary
Fixed a flaky test that was causing intermittent CI failures. The
test_stdio_initialize_and_tools_listtest had a ~20-30% failure rate due to a race condition.Problem
The test would fail with
StopIterationwhen trying to retrieve thetools/listresponse (id=2). This happened because:Solution
Added a proper MCP shutdown sequence to the test:
shutdownrequest (id=3)notifications/exitnotificationAlso improved error messages to show which responses were actually received when assertions fail.
Testing
References
Fixes the test failure from: https://github.com/teh-hippo/tiny-ntfy-mcp/actions/runs/24599264157/job/71935021663