mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
- 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).
114 lines
5.2 KiB
Python
114 lines
5.2 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_at_last_item_scrolls_down(self):
|
|
"""Cursor on Cancel (index 12) with 8-row window: offset = 12 - 8 + 1 = 5."""
|
|
offset = _compute_scroll_offset(12, 0, 8, self.N)
|
|
assert offset == 5
|
|
assert 12 in _visible_indices(12, 0, 8, self.N)
|
|
|
|
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_cursor_above_window_scrolls_up(self):
|
|
"""Cursor above current window: offset tracks cursor."""
|
|
# window currently shows [5..12], cursor moves to 3
|
|
offset = _compute_scroll_offset(3, 5, 8, self.N)
|
|
assert offset == 3
|
|
assert 3 in _visible_indices(3, 5, 8, self.N)
|
|
|
|
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_single_item_list(self):
|
|
"""Edge case: one choice, cursor 0."""
|
|
assert _compute_scroll_offset(0, 0, 8, 1) == 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
|