test(watch): script only the watch loop's sleep in drive_watch by madhusudanpatnaik · Pull Request #4620 · Graphify-Labs/graphify · GitHub
Skip to content

test(watch): script only the watch loop's sleep in drive_watch - #4620

Open
madhusudanpatnaik wants to merge 1 commit into
Graphify-Labs:v8from
madhusudanpatnaik:fix/watch-harness-sleep
Open

madhusudanpatnaik wants to merge 1 commit into
Graphify-Labs:v8from
madhusudanpatnaik:fix/watch-harness-sleep

Conversation

@madhusudanpatnaik

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes an intermittent CI failure in the watch tests. Test-only: nothing under graphify/ changes.

drive_watch (in tests/test_language_override_lifecycle.py, also used by tests/test_pr10_watch_overrides.py) scripts the watch loop by replacing time.sleep with an event pump. Each call dispatches the next scripted event, and once the script runs out it raises KeyboardInterrupt to stop watch(). It installed the pump with monkeypatch.setattr(watch_mod.time, "sleep", pump). watch_mod.time is the shared time module, so every sleeper in the process ran the pump, not only the watch loop.

A rebuild calls _git_head(), which is subprocess.run(["git", "rev-parse", "HEAD"], ..., timeout=3). When git has closed its output but hasn't been reaped yet, CPython's Popen._wait polls with time.sleep(). That sleep then consumed the next scripted event, or, after the last one, raised KeyboardInterrupt inside the rebuild. _git_head catches only Exception, so the interrupt propagated, watch() printed "Stopped." before extract() ran, and the last change never reached graph.json.

It failed this way in CI on #4580 (run 39500320206, test (3.13); the PR doesn't touch watch code):

FAILED tests/test_language_override_lifecycle.py::test_watch_root_config_lifecycle_rebuilds_and_refreshes_event_mapping[modify] - AssertionError: assert 'changed()' in {'initial()', 'page.tpl', 'stable()', 'stable.py'}

The fix replaces watch.py's own time reference with a small object whose sleep is the pump and whose other attributes resolve to the real module. The watch loop is still fully scripted, because its sleep and monotonic both go through watch_mod.time. subprocess and everything else keep the real time.sleep.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

Invariant: the script advances only when the watch loop itself sleeps.

  • Reproduced on v8 (0.9.93, b90b9df): I repeated the [modify] scenario 3,000 times in one process. It failed 3 times, each with the same labels as in CI. I logged Popen._try_wait around each rebuild. In every failure, the last rebuild's git rev-parse poll returned "not exited yet" just before the watcher stopped, which is the sleep path. In passing runs the poll returned "exited" on the first try.

  • With this PR: 3,000 runs, 0 failures. The race still occurred in 2 of them (4 subprocess sleeps during the final rebuild), and those runs passed.

  • Regression test test_drive_watch_scripts_only_the_watch_loop_sleep: a sleep happens inside _git_head during the rebuild, where subprocess's wait loop sleeps. On v8 it fails on every run, because the watcher stops mid-rebuild and graph.json is never written. It passes here.

  • No other test patches time.sleep this way. drive_watch is the only place.

  • Read the CONTRIBUTING.md guide.

  • Reproduced the issue and identified the invariant.

  • Made the smallest fix necessary.

  • Added a regression test (if bug fix) or isolated boundary test.

  • Kept the PR description synchronized with the final implementation.

  • Documented any limitations / unsupported cases explicitly.

Limitation: _rebuild_lock's Windows retry (time.sleep(0.05), in watch.py) still goes through the pump, as it did before. It only runs on lock contention, which these tests don't create. I left it unchanged.

How was this tested?

uv run pytest tests/test_language_override_lifecycle.py tests/test_pr10_watch_overrides.py -q   # 26 passed
# the new test on v8 (0.9.93): fails (FileNotFoundError: graph.json); here: passes
# Full suite: per Python, `uv sync --all-extras --frozen`, then `uv run pytest tests/ -q --tb=short`.
#   3.10: 8464 | 3.13: 8463 passed; `graphify --help` and `graphify install` OK on both
#   only the 7 DNS-dependent test_security tests fail (no network here; same on v8)
uv run python -m tools.skillgen --check / --audit-coverage / --schema-singleton / --monolith-roundtrip / --always-on-roundtrip   # all OK
uv run ruff check .                       # v0.15.14, as pinned in .pre-commit-config.yaml
uv run pyright                            # 643 errors on v8 (0.9.93) and on this branch; none new

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments. (No source fragments touched; --check passes.)
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.

drive_watch replaced `sleep` on the shared `time` module with its event
pump, so every sleeper in the process ran the script, not just the watch
loop. A rebuild runs `git rev-parse HEAD` through subprocess.run(timeout=3),
whose wait loop sleeps when git has closed its output but has not been
reaped yet. That sleep then consumed the next scripted event, or raised the
end-of-script KeyboardInterrupt inside the rebuild; _git_head only catches
Exception, so the watcher stopped before extract() and the last change never
reached graph.json. In CI this surfaced as
test_watch_root_config_lifecycle_rebuilds_and_refreshes_event_mapping[modify]
failing with `'changed()' not in {'initial()', ...}`.

Patch watch.py's own `time` reference instead: sleep is scripted, every other
attribute resolves to the real module, and sleeps elsewhere stay real.

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

Copy link
Copy Markdown

@github-actions github-actions Bot added the contributor Pull request from a returning community contributor label Oct 21, 2026

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; no formal proof of the changed code.


No blocking issues. I didn't find anything I'd hold the merge for.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 0 functions depend on the 0 functions this change touches.

Health — grade A; no new coupling hotspots.

Verification — 0 functions in the blast radius were not formally verified this run (proofs are advisory here).

Diff could not be reliably attributed to this PR: the base/head merge-base was unreachable in the shallow clone, so detailed findings were skipped (rather than emit a two-dot diff, which would misattribute base-branch changes).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Test selection

Test selection

0 of 436 test file(s) selected (0%) via static blast radius.

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor Pull request from a returning community contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant