fix: archive browser model filter is suffix-only and drops matching models (#1709)
This commit is contained in:
@@ -618,7 +618,12 @@ def setup_session_routes(session_manager: SessionManager, config: dict, webhook_
|
||||
safe_search = search.replace('%', r'\%').replace('_', r'\_')
|
||||
q = q.filter(DbSession.name.ilike(f"%{safe_search}%", escape='\\'))
|
||||
if model:
|
||||
q = q.filter(DbSession.model.ilike(f"%{model}"))
|
||||
# Contains match (mirrors the name filter above). The old
|
||||
# f"%{model}" was a SUFFIX-only match, so filtering by "gpt-4"
|
||||
# dropped "gpt-4o" and over-matched on shared suffixes; it also
|
||||
# left LIKE wildcards in the user value unescaped.
|
||||
safe_model = model.replace('%', r'\%').replace('_', r'\_')
|
||||
q = q.filter(DbSession.model.ilike(f"%{safe_model}%", escape='\\'))
|
||||
total = q.count()
|
||||
sort_map = {
|
||||
"recent": DbSession.updated_at.desc(),
|
||||
|
||||
76
tests/test_archived_sessions_model_filter.py
Normal file
76
tests/test_archived_sessions_model_filter.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Archive browser model filter must be a CONTAINS match, not suffix-only.
|
||||
|
||||
list_archived_sessions filtered with DbSession.model.ilike(f"%{model}") - a
|
||||
suffix match. Filtering by "gpt-4" therefore returned "openai/gpt-4" but
|
||||
silently DROPPED "gpt-4o" (contains but does not end with the value), and
|
||||
over-matched models that merely share the suffix. The sibling name filter
|
||||
already uses a wildcard-escaped contains match.
|
||||
"""
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
import core.database as cdb
|
||||
from core.database import Session as DbSession
|
||||
|
||||
_TMPDB = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
||||
_ENGINE = create_engine(
|
||||
f"sqlite:///{_TMPDB.name}",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=NullPool,
|
||||
)
|
||||
cdb.Base.metadata.create_all(_ENGINE)
|
||||
_TS = sessionmaker(bind=_ENGINE, autoflush=False, autocommit=False)
|
||||
|
||||
|
||||
def _route(router, path, method="GET"):
|
||||
for r in router.routes:
|
||||
if r.path == path and method in getattr(r, "methods", set()):
|
||||
return r.endpoint
|
||||
raise AssertionError(f"route not found: {path}")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def archived_endpoint(monkeypatch):
|
||||
import routes.session_routes as sr
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
monkeypatch.setattr(sr, "SessionLocal", _TS)
|
||||
monkeypatch.setattr(sr, "effective_user", lambda request: "alice")
|
||||
router = sr.setup_session_routes(MagicMock(), {})
|
||||
return _route(router, "/api/sessions/archived")
|
||||
|
||||
|
||||
def _seed(owner, *models):
|
||||
db = _TS()
|
||||
try:
|
||||
db.query(DbSession).delete()
|
||||
for m in models:
|
||||
db.add(DbSession(id=str(uuid.uuid4()), owner=owner, name=f"chat {m}",
|
||||
model=m, archived=True))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_contains_match_returns_all_models_sharing_the_substring(archived_endpoint):
|
||||
_seed("alice", "openai/gpt-4", "gpt-4o", "claude-3")
|
||||
res = archived_endpoint(request=None, model="gpt-4")
|
||||
got = {s["model"] for s in res["sessions"]}
|
||||
assert got == {"openai/gpt-4", "gpt-4o"}
|
||||
|
||||
|
||||
def test_exact_full_model_still_matches(archived_endpoint):
|
||||
_seed("alice", "openai/gpt-4", "gpt-4o")
|
||||
res = archived_endpoint(request=None, model="openai/gpt-4")
|
||||
assert {s["model"] for s in res["sessions"]} == {"openai/gpt-4"}
|
||||
|
||||
|
||||
def test_wildcard_in_filter_is_escaped(archived_endpoint):
|
||||
_seed("alice", "gpt-4o", "gpt_4o")
|
||||
res = archived_endpoint(request=None, model="gpt_4")
|
||||
assert {s["model"] for s in res["sessions"]} == {"gpt_4o"}
|
||||
Reference in New Issue
Block a user