mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
* 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.