Files
hermes-webui/tests/test_sessions_search_depth_validation.py
T
0b0a179e4c Release v0.51.231 — Release GY (stage-q1) (#3484)
## Release v0.51.231 — Release GY (stage-q1)

Four independent, low-risk fixes batched into one release. All gates green.

### Fixes
| PR | Author | Fix |
|----|--------|-----|
| #3368 | @nesquena-hermes | `/model` resolves models from the truncated `extra_models` tail of a large provider catalog (completes the half-done #3368 fix; @garyd9's `mimo-v2.5` case) |
| #3457 | @pix0127 | Auto-hide the Plugins settings tab when no plugins are installed; deep-link fallback to Conversation |
| #3474 | @Mubashirrrr | Guard malformed/negative `depth` on `/api/sessions/search` (no more 500 on `?depth=deep`, no silent newest-message drop on negative) |
| #3433 | @sjh9714 | Resolve symlinked home before the trust comparison so `~/` workspace suggestions work when the process home is a symlink |

### Note on #3457
The original contributor patch reassigned a `const section` (a **runtime `TypeError` brick** on the settings panel) and placed the hidden-tab fallback *after* the panes were already toggled. Refactored to `let` + hoist the fallback above all consumers of `section`. ESLint runtime gate now clean.

### Gate results
- **Full pytest suite**: 7441 passed, 7 skipped, 3 xpassed, **0 failed**
- **ESLint runtime gate**: CLEAN (caught + fixed the #3457 const-reassign)
- **ruff forward gate**: CLEAN (no new violations on changed lines)
- **browser-smoke gate**: CLEAN (`/`, `/#settings`, `/#sessions` — zero console errors)
- **Codex (regression)**: SAFE TO SHIP
- **Opus (correctness)**: SAFE TO SHIP

Closes #3368. Closes #3457. Closes #3433.

Co-authored-by: pix0127 <pix0127@users.noreply.github.com>
Co-authored-by: Mubashirrrr <Mubashirrrr@users.noreply.github.com>
Co-authored-by: sjh9714 <sjh9714@users.noreply.github.com>
2026-06-03 09:13:01 -07:00

73 lines
3.0 KiB
Python

"""Regression: a malformed/negative ``depth`` on the session content-search
endpoint must not crash or silently exclude the newest messages.
``GET /api/sessions/search?...&depth=<x>`` parsed ``depth`` with a bare
``int()``. A non-numeric value (e.g. ``?depth=deep``) raised ValueError, which
propagated to the top-level request handler and surfaced as a generic HTTP 500.
``depth`` caps how many leading messages are scanned per session
(``sess.messages[:depth]``). A negative value sliced as ``messages[:-n]``,
silently dropping the *most recent* messages from the search instead of capping
the scan — so a match in a session's latest turn would be missed. depth is now
clamped to ``>= 0`` (0 keeps its existing "search the whole transcript"
meaning), mirroring the guard sibling handlers already use.
"""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import patch
from urllib.parse import urlparse
def _run_search(query):
"""Invoke _handle_sessions_search against one synthetic session whose match
lives in its LAST message, capturing the JSON payload/status."""
import api.routes as routes
sessions_meta = [{"session_id": "s1", "title": "Untitled"}]
session = SimpleNamespace(
session_id="s1",
messages=[
{"role": "user", "content": "first message"},
{"role": "assistant", "content": "second message"},
{"role": "user", "content": "NEEDLE in the latest message"},
],
)
captured = {}
def fake_j(handler, payload, status=200, extra_headers=None):
captured["status"] = status
captured["payload"] = payload
with patch("api.routes.all_sessions", return_value=list(sessions_meta)), patch(
"api.routes.get_session", return_value=session
), patch("api.routes.j", side_effect=fake_j):
routes._handle_sessions_search(SimpleNamespace(), urlparse(query))
return captured
def test_search_non_numeric_depth_does_not_500():
# Before the fix this raised ValueError -> 500.
captured = _run_search("/api/sessions/search?q=needle&content=1&depth=deep")
assert captured["status"] == 200
# depth falls back to 5 (>= 3 messages here), so the needle is found.
assert captured["payload"]["count"] == 1
def test_search_negative_depth_still_scans_newest_message():
# depth=-2 with 3 messages: an unclamped messages[:-2] scan would look only
# at the first message and MISS the needle in the latest one. Clamped to a
# default >= 0, the newest message is searched and the match is found.
captured = _run_search("/api/sessions/search?q=needle&content=1&depth=-2")
assert captured["status"] == 200
assert captured["payload"]["count"] == 1
def test_search_valid_depth_still_caps_scan():
# depth=1 scans only the first message, which does NOT contain the needle,
# so no match — proving the cap still works for well-formed input.
captured = _run_search("/api/sessions/search?q=needle&content=1&depth=1")
assert captured["status"] == 200
assert captured["payload"]["count"] == 0