Files
hermes-agent/scripts
Teknium 74f77ab9b0 Code health blocks new wall-clock sleeps in tests (HX013), adds a shared wait_until helper (#136644)
* feat(code-health): HX013 blocks new wall-clock sleeps in tests

A fixed time.sleep()/asyncio.sleep() is a bet that the code under test finishes
in time; on a loaded runner it loses and the test flakes. Main has ~1,470 of
them in tests/ and nothing stopped new ones (HX rules exclude tests).

HX013 runs only on test paths (Rule.include) and flags time/asyncio/anyio/trio
sleeps with a delay that is not a literal 0, plus a fresh threading.Event().wait(x).
Existing sleeps are grandfathered by the ratchet's base-relative compare; moved
code keeps its debt; '# health: allow HX013 -- <why>' waives a deliberate delay.

The fix text points at tests/waiting.py (wait_until / async_wait_until), a shared
poll-with-deadline helper replacing the ~60 private copies across the suite.

* fix(code-health): HX013 passes the interval of a poll under a deadline

The frozen replay (PRs merged 2026-10-08..09) hit three private poll helpers
(while not done and monotonic() < end: sleep(x)): that is the compliant shape the
fix text recommends, not a fixed delay. A while loop that reads a clock and can
end on a non-clock condition now exempts its sleep; a loop that only waits out
the clock, or polls with no deadline, still counts. tests/waiting.py needs no
waiver any more; tests/AGENTS.md names the rule and the helper.

* fix(code-health): HX013 passes a round-bounded poll loop

The second frozen window (2026-10-06..07) hit 'for _ in range(200): await
asyncio.sleep(0.01); if idle: return', a poll bounded by its round count: load
stretches it but cannot make it return early. A for-range loop whose body can
leave on a non-clock condition now exempts its sleep; a range loop of fixed
delays, or a poll over arbitrary items, still counts.

* fix(code-health): HX013 exempts only a real poll's own interval

Review fixes for the HX013 poll-loop exemption, plus a file split:

- Split: the HX013 checker moves to scripts/code_health/py_rules_sleep.py,
  and the AST helpers it shares with py_rules (Ctx, _FUNCS, _dotted,
  _call_name, _deferred_parts, _eager) move to a leaf module py_ast.py.
  py_rules.py drops from 1,034 to 877 lines (928 on main); CHECKERS still
  registers HX013.
- Clock-only conditions: only a test operand that reads something other
  than the clock counts as a condition. Combined clock checks
  (`monotonic() < a and monotonic() < b`), `not`/constant operands, and
  locals assigned only from clock arithmetic (`now < end`) no longer
  exempt the sleep. `and`/`or`/`not` follow their logic, so `while not
  done() or monotonic() < end` still waits out the clock.
- Loop ownership: a `break` ends only the loop whose body holds it (a
  nested loop's break does not count for the outer loop; one in the
  nested loop's `else:` does). Nested defs, lambdas and classes are
  skipped. A sleep in a loop's `else:`, `while` test or `for` iterable is
  not that loop's interval.
- Retry polls: a `try` body's return/break counts as conditional when the
  body calls something that can raise (`try: return probe() except
  OSError: sleep`). A body that only sleeps still counts as a fixed delay.

On origin/main this flags 2 more grandfathered sleeps and exempts 12 retry
polls the old rule missed, so the count goes from 923 to 913.
2026-10-11 11:27:22 -07:00
..
2026-10-09 17:34:36 +02:00
2026-10-09 17:34:38 +02:00
2026-10-08 21:29:48 +02:00