Skip to content

test: stop watch tests patching the global time.sleep - #1183

Merged
1 commit merged into
stagingfrom
fix/test-sleep-patch-leak
Nov 9, 2026
Merged

1 commit merged into
stagingfrom
fix/test-sleep-patch-leak

Conversation

@tirth8205

Copy link
Copy Markdown
Owner

What went wrong

CI on Python 3.11 and 3.12 once stopped at about 65% with a KeyboardInterrupt raised from unittest/mock.py.

Several watch tests ended the watch loop with patch("time.sleep", side_effect=KeyboardInterrupt). That patches sleep for the whole process. subprocess.run(timeout=...) also calls time.sleep while it waits for a child to exit. On a busy runner the child has not exited on the first poll, so a call in watch startup hit the patched sleep, the interrupt escaped watch(), and pytest took it as Ctrl+C and stopped the run.

Note: patch("code_review_graph.<module>.time.sleep") would not help. That path names the same shared time module, so it is still the global sleep.

Fix (tests only)

watch() already has a seam: stop_event. The loop waits on it once per tick and nothing else calls it, so no code under test can reach it.

  • Startup tests pass an event that is already set, so the loop ends at its first tick.
  • test_cli_watch_with_the_pair_keeps_running_on_a_never_embedded_graph drives cli.main(), which passes no event, so it wraps incremental.watch to add one.
  • test_watch_robustness.py's tick driver is now a stop event whose wait() is one tick: it runs one scripted step, then raises KeyboardInterrupt like a real Ctrl+C. It had the same leak: a sleep inside a subprocess call could run a scripted step early.

Tests changed: test_postprocessing.py (2), test_incremental.py (2), test_watch_refresh_without_index.py (1), test_embedding_refresh_skip_notice.py (1), test_watch_robustness.py (tick driver plus 1). No production change. No raising patch of the global time.sleep is left in tests/. No CHANGELOG entry, as only tests change.

Proof

A small pytest plugin forces the subprocess wait path: it makes Popen._try_wait(WNOHANG) report "still running" once per process, which is what a loaded runner does by chance.

Before (on staging), with the plugin:

  • test_postprocessing.py and test_embedding_refresh_skip_notice.py: !!! KeyboardInterrupt !!! at unittest/mock.py:1228, run stopped after 20 tests. Each module triggers it on its own.
  • test_watch_robustness.py: test_normal_file_change_still_updates_the_graph fails 3 runs out of 3 (a subprocess sleep runs a tick early).

After, with the plugin:

  • The 5 changed modules: 286 passed (Python 3.13, and Python 3.11).
  • test_watch_robustness.py and test_postprocessing.py: 97 passed, 3 runs out of 3.

Gates: uv run pytest tests/ --tb=short -q: 6390 passed, 900 skipped, 3 xfailed, 4 xpassed. ruff check code_review_graph/ clean (changed test files clean too). mypy: no issues.

🤖 Generated with Claude Code

Watch tests stopped the loop with patch("time.sleep",
side_effect=KeyboardInterrupt). That patches sleep for the whole
process, and subprocess.run(timeout=...) sleeps while it waits for a
child. On a busy runner a git call in watch startup hit the patched
sleep, the KeyboardInterrupt escaped, and pytest stopped the whole run
(seen at about 65% on Python 3.11 and 3.12).

The loop already has a seam: watch(stop_event=...). Tests now pass an
event that is already set, or, where they script ticks or need a real
Ctrl+C path, a small driver whose wait() is one tick. Only the watch
loop calls it. The tick driver had the same leak: a git sleep could run
a scripted tick early.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

code-review-graph review

Overall risk: 0.30 (LOW) — 24 changed function(s)/class(es), 0 affected flow(s), 0 test gap(s)

Risk-scored changes

Risk Level Symbol Location Tested
0.30 low tests/test_embedding_refresh_skip_notice.py::TestWatchTellsTheUser.watch_one_tick tests/test_embedding_refresh_skip_notice.py:473 (test)
0.30 low tests/test_watch_robustness.py::_TickDriver.__init__ tests/test_watch_robustness.py:138 (test)
0.30 low tests/test_watch_robustness.py::_TickDriver.wait tests/test_watch_robustness.py:142 (test)
0.15 low tests/test_watch_robustness.py::_TickDriver tests/test_watch_robustness.py:126 (test)
0.15 low tests/test_watch_robustness.py::TestWatchLoop._watch_with tests/test_watch_robustness.py:955 (test)
0.05 low tests/test_embedding_refresh_skip_notice.py::TestWatchTellsTheUser tests/test_embedding_refresh_skip_notice.py:418 (test)
0.05 low tests/test_embedding_refresh_skip_notice.py::TestWatchTellsTheUser.test_cli_watch_with_the_pair_keeps_runn... tests/test_embedding_refresh_skip_notice.py:451 (test)
0.05 low tests/test_incremental.py::TestWatchReconciliation tests/test_incremental.py:1795 (test)
0.05 low tests/test_incremental.py::TestWatchReconciliation.test_watch_reconciles_before_observer_startup tests/test_incremental.py:1842 (test)
0.05 low tests/test_incremental.py::TestWatchReconciliation.test_watch_startup_postprocess_warning_stops_observer tests/test_incremental.py:1865 (test)

Token savings: this graph-backed report used ~56,437 fewer tokens (~94%) than reading every changed file in full (estimated, chars/4 approximation).


Powered by code-review-graph — local-first analysis; no code leaves the CI runner.

@tirth8205 tirth8205 closed this pull request by merging all changes into staging in d01c1fd Nov 9, 2026
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.

1 participant