diff --git a/harness/tests/test_qdrant_vector_store.py b/harness/tests/test_qdrant_vector_store.py index af575b03..a4c6556a 100644 --- a/harness/tests/test_qdrant_vector_store.py +++ b/harness/tests/test_qdrant_vector_store.py @@ -892,3 +892,73 @@ def test_scroll_accepts_null_or_missing_terminal_offset(next_page_offset): expected_dimension=1024, request=request, ) assert store.existing_hashes("memory", ["memory"]) == {} + + +_QUERY_PAYLOAD = { + "record_key": "memory:1", + "record_kind": "memory", + "kind": "memory", + "ref": "ref:memory:1", + "title": "title:memory:1", + "content": "content:memory:1", +} + + +def _query_response_store(points): + fake = FakeQdrantHttp() + fake.collection = {"vectors": {"size": 1024, "distance": "Cosine"}} + fake.payload_indexes = set(_REQUIRED_INDEXES) + original = fake.request + + def request(method, url, **kwargs): + if method == "POST" and url.endswith("/points/query"): + return FakeResponse(200, {"result": {"points": points}}) + return original(method, url, **kwargs) + + store = _store(fake) + store._request = request + return store + + +@pytest.mark.parametrize("leaf", [ + "record_key", "record_kind", "kind", "ref", "title", "content", +]) +def test_query_rejects_non_string_required_vector_hit_payload_leaves(leaf): + payload = {**_QUERY_PAYLOAD, leaf: 1} + store = _query_response_store([{"id": "p1", "score": 0.9, "payload": payload}]) + + with pytest.raises(VectorResponseError): + store.search(["memory"], [0.2] * 1024, limit=1, kinds=["memory"]) + + +@pytest.mark.parametrize("leaf", [ + "record_key", "record_kind", "kind", "ref", "title", "content", +]) +def test_query_rejects_missing_required_vector_hit_payload_leaves(leaf): + payload = {key: value for key, value in _QUERY_PAYLOAD.items() if key != leaf} + store = _query_response_store([{"id": "p1", "score": 0.9, "payload": payload}]) + + with pytest.raises(VectorResponseError): + store.search(["memory"], [0.2] * 1024, limit=1, kinds=["memory"]) + + +def test_query_rejects_heterogeneous_record_keys_before_sorting_hits(): + store = _query_response_store([ + {"id": "p1", "score": 0.9, "payload": _QUERY_PAYLOAD}, + { + "id": "p2", + "score": 0.8, + "payload": {**_QUERY_PAYLOAD, "record_key": 2}, + }, + ]) + + with pytest.raises(VectorResponseError): + store.search(["memory"], [0.2] * 1024, limit=2, kinds=["memory"]) + + +@pytest.mark.parametrize("score", [True, False, 10**1000, float("nan"), float("inf"), float("-inf")]) +def test_query_rejects_scores_that_cannot_be_finite_float(score): + store = _query_response_store([{"id": "p1", "score": score, "payload": _QUERY_PAYLOAD}]) + + with pytest.raises(VectorResponseError): + store.search(["memory"], [0.2] * 1024, limit=1, kinds=["memory"]) diff --git a/harness/tht/adapters/vector/qdrant.py b/harness/tht/adapters/vector/qdrant.py index 91045b2f..c1f91e09 100644 --- a/harness/tht/adapters/vector/qdrant.py +++ b/harness/tht/adapters/vector/qdrant.py @@ -1,5 +1,6 @@ """Qdrant-backed vector store for one workspace-owned semantic collection.""" +import math import re from collections.abc import Callable from uuid import NAMESPACE_URL, uuid5 @@ -477,16 +478,25 @@ class QdrantVectorStore: raise VectorResponseError("Qdrant returned malformed query response") payload = point.get("payload") score = point.get("score") + required_payload_leaves = ( + "record_key", "record_kind", "kind", "ref", "title", "content" + ) if ( not isinstance(payload, dict) or type(score) not in (int, float) or any( - key in payload and not isinstance(payload[key], str) - for key in ("record_key", "record_kind", "kind", "ref", "title", "content") + key not in payload or not isinstance(payload[key], str) + for key in required_payload_leaves ) ): raise VectorResponseError("Qdrant returned malformed query response") - return hit_from_metadata(float(score), payload) + try: + similarity = float(score) + except (OverflowError, ValueError) as exc: + raise VectorResponseError("Qdrant returned malformed query response") from exc + if not math.isfinite(similarity): + raise VectorResponseError("Qdrant returned malformed query response") + return hit_from_metadata(similarity, payload) def _call(self, method: str, path: str, payload: dict | None, allow_missing: bool = False) -> dict | None: try: