fix: prevent P2 from owning Qdrant lifecycle

This commit is contained in:
2026-08-11 07:00:05 +02:00
parent f5e76fff53
commit e7ac22f0dc
5 changed files with 179 additions and 5 deletions
+84 -2
View File
@@ -33,6 +33,7 @@ class FakeQdrantHttp:
self.distance = distance
self.collection = None
self.payload_indexes: set[str] = set()
self.payload_index_types: dict[str, str] = {}
self.points: dict[str, dict] = {}
self.calls: list[tuple[str, str, dict | None]] = []
self.fail_request: Exception | None = None
@@ -59,7 +60,7 @@ class FakeQdrantHttp:
}
},
"payload_schema": {
field: {"data_type": "keyword"} for field in sorted(self.payload_indexes)
field: {"data_type": self.payload_index_types.get(field, "keyword")} for field in sorted(self.payload_indexes)
},
}
})
@@ -75,6 +76,8 @@ class FakeQdrantHttp:
return FakeResponse(200, {"status": "ok"})
if method == "PUT" and path == "/collections/workspace-semantic/points":
if self.collection is None:
return FakeResponse(404, {"status": "error"})
for point in json["points"]:
self.points[point["id"]] = point
return FakeResponse(200, {"result": {"status": "acknowledged"}})
@@ -161,13 +164,14 @@ def _write_record(record_id: str, kind: str, *, metadata=None):
)
def _store(fake: FakeQdrantHttp) -> QdrantVectorStore:
def _store(fake: FakeQdrantHttp, *, collection_lifecycle="create_if_missing") -> QdrantVectorStore:
return QdrantVectorStore(
base_url="http://qdrant:6333",
collection="workspace-semantic",
workspace_id="demo",
workspace_revision="a" * 40,
expected_dimension=1024,
collection_lifecycle=collection_lifecycle,
request=fake.request,
)
@@ -178,6 +182,84 @@ def test_point_id_is_deterministic_uuidv5():
)
def test_require_existing_refuses_missing_collection_without_mutations():
fake = FakeQdrantHttp()
store = _store(fake, collection_lifecycle="require_existing")
with pytest.raises(VectorStoreError, match="semantic_index_incompatible"):
store.upsert("memory", [_write_record("memory:1", "memory")])
assert [call for call in fake.calls if call[0] == "PUT"] == []
@pytest.mark.parametrize(
("dimension", "distance", "indexes", "index_types"),
[(384, "Cosine", set(), {}),
(1024, "Dot", set(), {}),
(1024, "Cosine", {"content_hash"}, {}),
(1024, "Cosine", {
"content_hash", "document_id", "kind", "record_key", "record_kind",
"vector_generation", "workspace_id", "workspace_revision",
}, {"kind": "integer"})],
)
def test_require_existing_refuses_incompatible_collection_without_mutations(
dimension, distance, indexes, index_types
):
fake = FakeQdrantHttp(dimension=dimension, distance=distance)
fake.collection = {"vectors": {"size": dimension, "distance": distance}}
fake.payload_indexes = indexes
fake.payload_index_types = index_types
store = _store(fake, collection_lifecycle="require_existing")
with pytest.raises(VectorStoreError, match="semantic_index_incompatible"):
store.upsert("memory", [_write_record("memory:1", "memory")])
assert [call for call in fake.calls if call[0] == "PUT"] == []
def test_require_existing_writes_compatible_collection_without_lifecycle_mutations():
fake = FakeQdrantHttp()
fake.collection = {"vectors": {"size": 1024, "distance": "Cosine"}}
fake.payload_indexes = {
"content_hash", "document_id", "kind", "record_key", "record_kind",
"vector_generation", "workspace_id", "workspace_revision",
}
store = _store(fake, collection_lifecycle="require_existing")
assert store.upsert("memory", [_write_record("memory:1", "memory")]) == 1
assert not [call for call in fake.calls if call[0] == "PUT" and call[1].endswith("/index")]
def test_require_existing_write_fails_after_collection_is_deleted_without_recreating():
fake = FakeQdrantHttp()
fake.collection = {"vectors": {"size": 1024, "distance": "Cosine"}}
fake.payload_indexes = {
"content_hash", "document_id", "kind", "record_key", "record_kind",
"vector_generation", "workspace_id", "workspace_revision",
}
original_request = fake.request
deleted = False
def request(method, url, **kwargs):
nonlocal deleted
response = original_request(method, url, **kwargs)
if method == "GET" and url.endswith("/collections/workspace-semantic") and not deleted:
deleted = True
fake.collection = None
return response
store = QdrantVectorStore(
base_url="http://qdrant:6333", collection="workspace-semantic", workspace_id="demo",
workspace_revision="a" * 40, expected_dimension=1024,
collection_lifecycle="require_existing", request=request,
)
with pytest.raises(VectorStoreError):
store.upsert("memory", [_write_record("memory:1", "memory")])
assert not [call for call in fake.calls if call[0] == "PUT" and call[1].endswith("/collections/workspace-semantic")]
def test_upsert_creates_collection_and_keyword_indexes_idempotently():
fake = FakeQdrantHttp()
store = _store(fake)