* perf(ui): single-pass sidebar session row partitioning (#3658) Co-authored-by: Pamnard <pamnard@users.noreply.github.com> * fix(search): scope session search to active profile (#3646) Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com> * fix(ctl): tree-kill ctl.sh stop on Windows (#3670) Co-authored-by: Rod Boev <rod.boev@gmail.com> * docs(changelog): v0.51.269 — Release IK (stage-b2) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Pamnard <pamnard@users.noreply.github.com> Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com> Co-authored-by: Rod Boev <rod.boev@gmail.com>
75 lines
3.1 KiB
Python
75 lines
3.1 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", "profile": "default"}]
|
|
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.profiles.get_active_profile_name", return_value="default"), 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
|