From fc3e0f28f607246d98dfd3ca73b301173159ac43 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 4 Jun 2026 19:39:46 +0200 Subject: [PATCH] fix: add metrics-interval validator + type/test gaps (review #850) - Add Validator("VECTOR_SYNC_METRICS_REFRESH_INTERVAL", gte=1) so a 0/negative value can't turn the publish loop into a busy-spin. - Annotate count_indexed's qdrant_client param as AsyncQdrantClient. - Add tests: exact kwarg is forwarded to qdrant count, and the placeholder filter matches False (excludes placeholders) with chunk_index pinned to 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- nextcloud_mcp_server/config.py | 1 + .../vector/metrics_publisher.py | 3 ++- tests/unit/vector/test_metrics_publisher.py | 25 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/nextcloud_mcp_server/config.py b/nextcloud_mcp_server/config.py index 0a09b378..334508cb 100644 --- a/nextcloud_mcp_server/config.py +++ b/nextcloud_mcp_server/config.py @@ -271,6 +271,7 @@ _dynaconf = Dynaconf( Validator("VECTOR_SYNC_SCAN_INTERVAL", gte=1), Validator("VECTOR_SYNC_PROCESSOR_WORKERS", gte=1), Validator("VECTOR_SYNC_QUEUE_MAX_SIZE", gte=1), + Validator("VECTOR_SYNC_METRICS_REFRESH_INTERVAL", gte=1), Validator("VECTOR_SYNC_USER_POLL_INTERVAL", gte=1), Validator("VERIFICATION_CONCURRENCY", gte=1), Validator("DOCUMENT_CHUNK_SIZE", gte=1), diff --git a/nextcloud_mcp_server/vector/metrics_publisher.py b/nextcloud_mcp_server/vector/metrics_publisher.py index bee70bc1..87998201 100644 --- a/nextcloud_mcp_server/vector/metrics_publisher.py +++ b/nextcloud_mcp_server/vector/metrics_publisher.py @@ -24,6 +24,7 @@ from typing import Any import anyio from anyio.abc import TaskStatus +from qdrant_client import AsyncQdrantClient from qdrant_client.models import FieldCondition, Filter, MatchValue from nextcloud_mcp_server.config import get_settings @@ -41,7 +42,7 @@ logger = logging.getLogger(__name__) async def count_indexed( - qdrant_client, collection: str, *, exact: bool = True + qdrant_client: AsyncQdrantClient, collection: str, *, exact: bool = True ) -> tuple[int, int]: """Return ``(documents, chunks)`` indexed in the collection. diff --git a/tests/unit/vector/test_metrics_publisher.py b/tests/unit/vector/test_metrics_publisher.py index 784f0411..77ad7ddf 100644 --- a/tests/unit/vector/test_metrics_publisher.py +++ b/tests/unit/vector/test_metrics_publisher.py @@ -53,6 +53,31 @@ class TestCountIndexed: assert _must_keys(chunks_filter) == ["is_placeholder"] assert _must_keys(docs_filter) == ["is_placeholder", "chunk_index"] + async def test_placeholder_filter_excludes_placeholders(self) -> None: + # is_placeholder must match False (exclude), not True (which would count + # the in-flight placeholders as if they were indexed content). + qc = AsyncMock() + qc.count.side_effect = [_count_obj(10), _count_obj(3)] + + await mp.count_indexed(qc, _COLLECTION) + + chunks_filter = qc.count.await_args_list[0].kwargs["count_filter"] + assert chunks_filter.must[0].match.value is False + docs_filter = qc.count.await_args_list[1].kwargs["count_filter"] + # And the distinct-document filter pins chunk_index to 0. + assert docs_filter.must[0].match.value is False + assert docs_filter.must[1].match.value == 0 + + async def test_exact_kwarg_forwarded(self) -> None: + # The gauge path passes exact=False; dropping it would silently make the + # every-N-seconds refresh do exact counts on large tenants. + qc = AsyncMock() + qc.count.side_effect = [_count_obj(10), _count_obj(3)] + + await mp.count_indexed(qc, _COLLECTION, exact=False) + + assert all(call.kwargs["exact"] is False for call in qc.count.await_args_list) + class TestPublishVectorSyncMetrics: @pytest.fixture(autouse=True)