hermes-agent/tests/test_model_picker_scroll.py
Teknium 28524adb0e fix(tests): eliminate flaky/broken tests — shadow sys.path inserts, unmocked network in compressor tests, stale-SDK feishu pin guard, quadratic redact regexes
- Remove tests/-shadowing sys.path.insert(dirname/'..') from 11 test files:
  it prepended the tests/ dir itself to sys.path, so 'import agent' /
  'import hermes_cli' resolved to the test packages and collection died
  with ModuleNotFoundError depending on import order (2 files failed in
  every full-suite run; 9 more were latent).
- Patch call_llm in 5 context-compressor tests that called compress()
  unmocked: each burned ~50s attempting live LLM traffic through the
  relay before falling back (572s file — the slowest in the suite, and
  flaky under the 300s per-file timeout). File now runs in ~5s.
- agent/redact.py: fix two catastrophically-backtracking regexes hit by
  the compressor's redaction pass on large payloads —
  _STRICT_URL_USERINFO_RE anchors on the mandatory '//' (optional-scheme
  prefix backtracked O(n^2): ~55s on a 320KB payload, now sub-ms;
  output-equivalence fuzz-verified on 20k random strings), and the
  _CFG_DOTTED_RE/_CFG_ANCHORED_RE subs gain an exact linear keyword
  pre-gate so secret-free text skips the quadratic pattern entirely.
- tests/gateway/test_feishu.py: version-guard the extra_ua_tags SDK
  signature check; the repo pins lark-oapi==1.6.8 but stale local
  installs (1.5.3) fail the assertion — skip below the pin.
- tests/tools/test_managed_browserbase_and_modal.py: stub
  agent.redact + agent.credential_persistence in the fake agent package
  (empty __path__ blocks all real agent.* imports added since the fake
  was written).
- tests/gateway/test_startup_restart_race.py: raise wait_for timeouts
  2s -> 30s; 2s wall-clock on a loaded 40-worker box flaked in the
  baseline run (passes instantly when the box is quiet).
2026-07-29 15:12:28 -07:00

100 lines
4.5 KiB
Python

"""Tests for the scrolling viewport logic in _curses_prompt_choice (issue #5755).
The "More providers" submenu has 13 entries (11 extended + custom + cancel).
Before the fix, _curses_prompt_choice rendered items starting unconditionally
from index 0 with no scroll offset. On terminals shorter than ~16 rows, items
near the bottom were never drawn. When the cursor wrapped from 0 to the last
item (Cancel) via UP-arrow, the highlight rendered off-screen, leaving the menu
looking like only "Cancel" existed.
The fix adds a scroll_offset that tracks the cursor so the highlighted item
is always within the visible window. These tests exercise that logic in
isolation without requiring a real TTY.
"""
# ---------------------------------------------------------------------------
# Pure scroll-offset logic extracted from _curses_menu for unit testing
# ---------------------------------------------------------------------------
def _compute_scroll_offset(cursor: int, scroll_offset: int, visible: int, n_choices: int) -> int:
"""Mirror of the scroll adjustment block inside _curses_menu."""
if cursor < scroll_offset:
scroll_offset = cursor
elif cursor >= scroll_offset + visible:
scroll_offset = cursor - visible + 1
scroll_offset = max(0, min(scroll_offset, max(0, n_choices - visible)))
return scroll_offset
def _visible_indices(cursor: int, scroll_offset: int, visible: int, n_choices: int):
"""Return the list indices that would be rendered for the given state."""
scroll_offset = _compute_scroll_offset(cursor, scroll_offset, visible, n_choices)
return list(range(scroll_offset, min(scroll_offset + visible, n_choices)))
# ---------------------------------------------------------------------------
# Tests: scroll offset calculation
# ---------------------------------------------------------------------------
class TestScrollOffsetLogic:
N = 13 # typical extended-providers list length
def test_cursor_at_zero_no_scroll(self):
"""Start position: offset stays 0, first items visible."""
assert _compute_scroll_offset(0, 0, 8, self.N) == 0
def test_cursor_within_window_unchanged(self):
"""Cursor inside the current window: offset unchanged."""
assert _compute_scroll_offset(5, 0, 8, self.N) == 0
def test_cursor_wraps_to_cancel_via_up(self):
"""UP from index 0 wraps to last item; last item must be visible."""
wrapped_cursor = (0 - 1) % self.N # == 12
indices = _visible_indices(wrapped_cursor, 0, 8, self.N)
assert wrapped_cursor in indices
def test_visible_window_never_exceeds_list(self):
"""Offset is clamped so the window never starts past the list end."""
offset = _compute_scroll_offset(12, 0, 20, self.N) # window larger than list
assert offset == 0
def test_list_fits_in_window_no_scroll_needed(self):
"""If all choices fit in the visible window, offset is always 0."""
for cursor in range(self.N):
offset = _compute_scroll_offset(cursor, 0, 20, self.N)
assert offset == 0, f"cursor={cursor} should not scroll when window > list"
def test_cursor_always_in_visible_range(self):
"""Invariant: cursor is always within the rendered window after adjustment."""
visible = 5
for cursor in range(self.N):
indices = _visible_indices(cursor, 0, visible, self.N)
assert cursor in indices, f"cursor={cursor} not in visible={indices}"
def test_full_navigation_down_cursor_always_visible(self):
"""Simulate pressing DOWN through all items; cursor always in view."""
visible = 6
scroll_offset = 0
cursor = 0
for _ in range(self.N + 2): # wrap around twice
scroll_offset = _compute_scroll_offset(cursor, scroll_offset, visible, self.N)
rendered = list(range(scroll_offset, min(scroll_offset + visible, self.N)))
assert cursor in rendered, f"cursor={cursor} not in rendered={rendered}"
cursor = (cursor + 1) % self.N
def test_full_navigation_up_cursor_always_visible(self):
"""Simulate pressing UP through all items; cursor always in view."""
visible = 6
scroll_offset = 0
cursor = 0
for _ in range(self.N + 2):
scroll_offset = _compute_scroll_offset(cursor, scroll_offset, visible, self.N)
rendered = list(range(scroll_offset, min(scroll_offset + visible, self.N)))
assert cursor in rendered, f"cursor={cursor} not in rendered={rendered}"
cursor = (cursor - 1) % self.N