fix: close internal semantic review gaps
This commit is contained in:
@@ -212,6 +212,27 @@ def test_upsert_refuses_collection_dimension_or_distance_mismatch_without_recrea
|
||||
assert creates == []
|
||||
|
||||
|
||||
def test_health_fails_when_the_bound_collection_is_missing():
|
||||
fake = FakeQdrantHttp()
|
||||
|
||||
health = _store(fake).health()
|
||||
|
||||
assert health.ok is False
|
||||
assert health.read_reachable is False
|
||||
assert health.write_reachable is False
|
||||
assert "missing" in (health.detail or "").lower()
|
||||
|
||||
|
||||
def test_health_fails_when_required_payload_indexes_are_missing_without_creating_them():
|
||||
fake = FakeQdrantHttp()
|
||||
fake.collection = {"vectors": {"size": 1024, "distance": "Cosine"}}
|
||||
|
||||
health = _store(fake).health()
|
||||
|
||||
assert health.ok is False
|
||||
assert fake.payload_indexes == set()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("record", "semantic_kind"),
|
||||
[
|
||||
@@ -307,6 +328,90 @@ def test_existing_hashes_health_and_exact_generation_inventory_and_delete():
|
||||
assert health.dimension_compatible is True
|
||||
|
||||
|
||||
def test_metadata_search_rejects_a_workspace_id_different_from_the_bound_adapter():
|
||||
fake = FakeQdrantHttp()
|
||||
store = _store(fake)
|
||||
generation = "gen:" + "1" * 32
|
||||
store.upsert("evidence", [
|
||||
_write_record(
|
||||
f"demo:{generation}:chunk:1",
|
||||
"evidence",
|
||||
metadata={
|
||||
"workspace_id": "demo", "vector_generation": generation,
|
||||
"document_id": "doc:shared",
|
||||
},
|
||||
),
|
||||
])
|
||||
foreign = next(iter(fake.points.values())).copy()
|
||||
foreign["id"] = point_id("other", "evidence", f"other:{generation}:chunk:1")
|
||||
foreign["payload"] = {
|
||||
**foreign["payload"],
|
||||
"workspace_id": "other",
|
||||
"record_key": f"other:{generation}:chunk:1",
|
||||
"ref": "ref:foreign",
|
||||
"title": "foreign",
|
||||
"content": "foreign",
|
||||
}
|
||||
fake.points[foreign["id"]] = foreign
|
||||
|
||||
with pytest.raises(VectorStoreError, match="workspace namespace does not match"):
|
||||
store.search(
|
||||
["evidence"], [0.2] * 1024, limit=5, kinds=["evidence"],
|
||||
metadata_filter={
|
||||
"workspace_id": "other",
|
||||
"vector_generation": generation,
|
||||
"document_ids": ["doc:shared"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_generation_inventory_rejects_a_workspace_id_different_from_the_bound_adapter():
|
||||
fake = FakeQdrantHttp()
|
||||
store = _store(fake)
|
||||
|
||||
with pytest.raises(VectorStoreError, match="workspace namespace does not match"):
|
||||
store.list_evidence_generations("evidence", "other")
|
||||
assert not any(call[1].endswith("/points/scroll") for call in fake.calls)
|
||||
|
||||
|
||||
def test_generation_delete_cannot_mutate_foreign_workspace_or_non_evidence_points():
|
||||
fake = FakeQdrantHttp()
|
||||
store = _store(fake)
|
||||
generation = "gen:" + "1" * 32
|
||||
store.upsert("evidence", [
|
||||
_write_record(
|
||||
f"demo:{generation}:chunk:1",
|
||||
"evidence",
|
||||
metadata={
|
||||
"workspace_id": "demo", "vector_generation": generation,
|
||||
"document_id": "doc:demo",
|
||||
},
|
||||
),
|
||||
])
|
||||
demo = next(iter(fake.points.values()))
|
||||
foreign = demo.copy()
|
||||
foreign["id"] = point_id("other", "evidence", f"other:{generation}:chunk:1")
|
||||
foreign["payload"] = {
|
||||
**demo["payload"], "workspace_id": "other",
|
||||
"record_key": f"other:{generation}:chunk:1",
|
||||
}
|
||||
fake.points[foreign["id"]] = foreign
|
||||
memory = demo.copy()
|
||||
memory["id"] = point_id("other", "memory", "memory:foreign")
|
||||
memory["payload"] = {
|
||||
**demo["payload"], "workspace_id": "other", "kind": "memory",
|
||||
"record_kind": "memory", "record_key": "memory:foreign",
|
||||
}
|
||||
fake.points[memory["id"]] = memory
|
||||
before = set(fake.points)
|
||||
|
||||
with pytest.raises(VectorStoreError, match="workspace namespace does not match"):
|
||||
store.delete_generation("evidence", generation, "other")
|
||||
|
||||
assert set(fake.points) == before
|
||||
assert not any(call[1].endswith("/points/delete?wait=true") for call in fake.calls)
|
||||
|
||||
|
||||
def test_delete_kinds_is_workspace_scoped_and_preserves_other_semantic_kinds():
|
||||
fake = FakeQdrantHttp()
|
||||
store = _store(fake)
|
||||
|
||||
@@ -94,14 +94,11 @@ class QdrantVectorStore:
|
||||
expected_dimension=self._expected_dimension,
|
||||
)
|
||||
|
||||
dimensions = ()
|
||||
compatible = None
|
||||
if info is not None:
|
||||
dimension = info["config"]["params"]["vectors"]["size"]
|
||||
dimensions = (dimension,)
|
||||
compatible = (
|
||||
None if self._expected_dimension is None else dimensions == (self._expected_dimension,)
|
||||
)
|
||||
dimension = info["config"]["params"]["vectors"]["size"]
|
||||
dimensions = (dimension,)
|
||||
compatible = (
|
||||
None if self._expected_dimension is None else dimensions == (self._expected_dimension,)
|
||||
)
|
||||
return VectorHealth(
|
||||
ok=compatible is not False,
|
||||
read_configured=True,
|
||||
@@ -142,12 +139,11 @@ class QdrantVectorStore:
|
||||
or not isinstance(workspace_id, str)
|
||||
):
|
||||
raise VectorStoreError("Invalid vector metadata filter")
|
||||
filter_must = [
|
||||
{"key": "workspace_id", "match": {"value": workspace_id}},
|
||||
{"key": "record_kind", "match": {"any": allowed_record_kinds}},
|
||||
self._require_bound_workspace(workspace_id)
|
||||
filter_must.extend([
|
||||
{"key": "vector_generation", "match": {"value": generation}},
|
||||
{"key": "document_id", "match": {"any": document_ids}},
|
||||
]
|
||||
])
|
||||
response = self._call(
|
||||
"POST",
|
||||
f"/collections/{self._collection}/points/query",
|
||||
@@ -232,27 +228,19 @@ class QdrantVectorStore:
|
||||
raise VectorStoreError("Only exact Evidence generations may be deleted")
|
||||
if _WORKSPACE.fullmatch(workspace_id) is None:
|
||||
raise VectorStoreError("Invalid Evidence workspace namespace")
|
||||
self._require_bound_workspace(workspace_id)
|
||||
must = [
|
||||
*self._workspace_filter(),
|
||||
{"key": "record_kind", "match": {"any": ["evidence"]}},
|
||||
{"key": "vector_generation", "match": {"value": generation}},
|
||||
]
|
||||
before = len(
|
||||
self._scroll(
|
||||
[
|
||||
{"key": "workspace_id", "match": {"value": workspace_id}},
|
||||
{"key": "record_kind", "match": {"any": ["evidence"]}},
|
||||
{"key": "vector_generation", "match": {"value": generation}},
|
||||
]
|
||||
)
|
||||
self._scroll(must)
|
||||
)
|
||||
self._call(
|
||||
"POST",
|
||||
f"/collections/{self._collection}/points/delete?wait=true",
|
||||
{
|
||||
"filter": {
|
||||
"must": [
|
||||
{"key": "workspace_id", "match": {"value": workspace_id}},
|
||||
{"key": "record_kind", "match": {"any": ["evidence"]}},
|
||||
{"key": "vector_generation", "match": {"value": generation}},
|
||||
]
|
||||
}
|
||||
},
|
||||
{"filter": {"must": must}},
|
||||
)
|
||||
return before
|
||||
|
||||
@@ -261,9 +249,10 @@ class QdrantVectorStore:
|
||||
raise VectorStoreError("Only exact Evidence generations may be listed")
|
||||
if _WORKSPACE.fullmatch(workspace_id) is None:
|
||||
raise VectorStoreError("Invalid Evidence workspace namespace")
|
||||
self._require_bound_workspace(workspace_id)
|
||||
points = self._scroll(
|
||||
[
|
||||
{"key": "workspace_id", "match": {"value": workspace_id}},
|
||||
*self._workspace_filter(),
|
||||
{"key": "record_kind", "match": {"any": ["evidence"]}},
|
||||
]
|
||||
)
|
||||
@@ -279,6 +268,10 @@ class QdrantVectorStore:
|
||||
def _workspace_filter(self) -> list[dict]:
|
||||
return [{"key": "workspace_id", "match": {"value": self._workspace_id}}]
|
||||
|
||||
def _require_bound_workspace(self, workspace_id: str) -> None:
|
||||
if workspace_id != self._workspace_id:
|
||||
raise VectorStoreError("Evidence workspace namespace does not match bound workspace")
|
||||
|
||||
def _allowed_record_kinds(
|
||||
self, collections: list[str], kinds: list[str] | None
|
||||
) -> list[str]:
|
||||
@@ -303,7 +296,7 @@ class QdrantVectorStore:
|
||||
response = self._call("GET", f"/collections/{self._collection}", None, allow_missing=True)
|
||||
if response is None:
|
||||
if not strict:
|
||||
return None
|
||||
raise VectorStoreError("Qdrant collection is missing")
|
||||
self._call(
|
||||
"PUT",
|
||||
f"/collections/{self._collection}",
|
||||
@@ -329,6 +322,8 @@ class QdrantVectorStore:
|
||||
raise VectorStoreError("Qdrant collection configuration mismatch")
|
||||
for field_name in _KEYWORD_INDEXES:
|
||||
if field_name not in result.get("payload_schema", {}):
|
||||
if not strict:
|
||||
raise VectorStoreError("Qdrant collection payload indexes mismatch")
|
||||
self._call(
|
||||
"PUT",
|
||||
f"/collections/{self._collection}/index",
|
||||
|
||||
Reference in New Issue
Block a user