Files
ThothII/harness/tests/test_search_similar_kinds.py

124 lines
4.7 KiB
Python

"""L1: filtro `kinds` server-side su search_similar (fast-follow post active-memory).
`memory` e `solved_question` condividono la tabella pgvector: senza filtro nel
`WHERE` della RPC, il top-k della tabella mista puo' affamare la ricerca memorie
(e viceversa) perche' il filtro per kind avveniva solo client-side DOPO il taglio
a top_n. Questi test fissano il contratto client:
- il client manda `kinds` nel payload della RPC quando richiesto (filtro esatto);
- su un server legacy (funzione a 3 argomenti -> PostgREST 404) ritenta senza
`kinds`, lasciando il filtro al post-filter client-side esistente;
- RestSearcher inoltra i kinds alla RPC.
"""
import pytest
from tht.config import RestConfig
from tht.vectorstore.reader import RestSearcher
from tht.vectorstore.rest_client import VectorRestClient, VectorRestError
class _Resp:
def __init__(self, status_code=200, payload=None, text=""):
self.status_code = status_code
self._payload = [] if payload is None else payload
self.text = text or ("[]" if status_code == 200 else text)
@property
def ok(self):
return self.status_code < 400
def json(self):
if not self.ok:
return {"message": self.text}
return self._payload
def _client() -> VectorRestClient:
return VectorRestClient(RestConfig(base_url="https://v/", api_key="K-READ"))
def test_search_similar_sends_kinds_in_rpc_payload(monkeypatch):
seen = []
def fake_post(url, json=None, **kw):
seen.append(json)
return _Resp(payload=[{"similarity": 0.9, "metadata": {"kind": "memory"}}])
monkeypatch.setattr("tht.vectorstore.rest_client.requests.post", fake_post)
rows = _client().search_similar("memory", [0.1] * 4, 5, kinds=["memory"])
assert len(rows) == 1
assert seen[0]["kinds"] == ["memory"]
assert seen[0]["table_name"] == "memory"
assert seen[0]["limit_count"] == 5
def test_search_similar_omits_kinds_when_none(monkeypatch):
seen = []
def fake_post(url, json=None, **kw):
seen.append(json)
return _Resp()
monkeypatch.setattr("tht.vectorstore.rest_client.requests.post", fake_post)
_client().search_similar("memory", [0.1] * 4, 5)
assert "kinds" not in seen[0]
def test_search_similar_falls_back_without_kinds_on_legacy_404(monkeypatch):
# Server legacy: la funzione a 4 argomenti non esiste -> PostgREST 404 (PGRST202).
# Il client ritenta senza `kinds`; il filtro resta al post-filter client-side.
seen = []
def fake_post(url, json=None, **kw):
seen.append(json)
if "kinds" in json:
return _Resp(status_code=404, text="Could not find the function (PGRST202)")
return _Resp(payload=[{"similarity": 0.8, "metadata": {"kind": "memory"}}])
monkeypatch.setattr("tht.vectorstore.rest_client.requests.post", fake_post)
rows = _client().search_similar("memory", [0.1] * 4, 5, kinds=["memory"])
assert len(rows) == 1
assert len(seen) == 2
assert "kinds" in seen[0] and "kinds" not in seen[1]
def test_search_similar_reraises_non_404_with_kinds(monkeypatch):
def fake_post(url, json=None, **kw):
return _Resp(status_code=500, text="boom")
monkeypatch.setattr("tht.vectorstore.rest_client.requests.post", fake_post)
with pytest.raises(VectorRestError, match="HTTP 500"):
_client().search_similar("memory", [0.1] * 4, 5, kinds=["memory"])
def test_generation_filter_is_sent_exactly_and_legacy_404_fails_closed(monkeypatch):
calls = []
def fake_call(self, function, payload):
calls.append(payload)
raise VectorRestError("HTTP 404 missing filtered RPC")
monkeypatch.setattr(VectorRestClient, "_call", fake_call)
metadata_filter = {"vector_generation": "gen:abc", "document_ids": ["doc:1"]}
with pytest.raises(VectorRestError, match="404"):
_client().search_similar(
"evidence", [0.1] * 4, 5, kinds=["evidence"], metadata_filter=metadata_filter
)
assert calls == [{
"query_embedding": [0.1] * 4, "limit_count": 5, "table_name": "evidence",
"kinds": ["evidence"], "metadata_filter": metadata_filter,
}]
def test_rest_searcher_forwards_kinds_to_client():
calls = []
class FakeClient:
def search_similar(self, table_name, query_vec, top_n, kinds=None):
calls.append((table_name, top_n, kinds))
return [{"similarity": 0.7, "metadata": {"kind": "solved_question",
"record_key": "solved:s1"}}]
hits = RestSearcher(FakeClient()).search([0.1] * 4, top_n=3, kinds=["solved_question"])
assert calls == [("memory", 3, ["solved_question"])]
assert [h.kind for h in hits] == ["solved_question"]