Resolve the latest PR-review comment on the verify-on-read tag-gate work:
- tests: stringify note IDs in test_verify_on_read.py so SearchResult.id
matches production (scanner stringifies all IDs on write) — helper and the
keeps/deleted/mixed/dedupe assertions (blocking).
- tests: make the unshared-file negative control a PDF so the drop is
unambiguously "unshared", not a mime_type_filter mismatch.
- config: add Validator("VECTOR_SYNC_PDF_TAG", len_min=1) — an empty tag name
would make find_files_by_tag("") misbehave in the verifier and scanner.
- verification: correct the _verify_files comment — two batch fetches (tag
REPORT + EXCLUDED_TAGS lookup) are held under one semaphore slot; the
pure-Python intersection runs outside it.
- tests: de-duplicate the minimal-PDF constant into a shared PDF_BYTES in
tests/integration/conftest.py, imported by both integration modules.
Verified: ruff/format/ty/unit all green; the two integration modules
(10 tests) pass against a local Nextcloud (app-only, no MCP profile needed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
"""Pytest configuration for integration tests.
|
|
|
|
This conftest.py provides hooks and fixtures specific to integration tests,
|
|
including the --provider flag for RAG tests.
|
|
"""
|
|
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Valid provider names
|
|
VALID_PROVIDERS = ["openai", "ollama", "anthropic", "bedrock"]
|
|
|
|
# Canonical minimal valid PDF for integration tests. verify-on-read gates file
|
|
# results on the vector-index tag via
|
|
# find_files_by_tag(..., mime_type_filter="application/pdf"), so file fixtures
|
|
# must be PDFs (matching what the scanner indexes), not .txt. Shared here so the
|
|
# constant is defined once rather than drifting across test modules.
|
|
PDF_BYTES = (
|
|
b"%PDF-1.4\n"
|
|
b"1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n"
|
|
b"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n"
|
|
b"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 200 200]>>endobj\n"
|
|
b"trailer<</Root 1 0 R>>\n%%EOF\n"
|
|
)
|
|
|
|
|
|
def pytest_addoption(parser):
|
|
"""Add --provider command line option for RAG tests."""
|
|
parser.addoption(
|
|
"--provider",
|
|
action="store",
|
|
default=None,
|
|
choices=VALID_PROVIDERS,
|
|
help="LLM provider for RAG tests: openai, ollama, anthropic, bedrock",
|
|
)
|
|
|
|
|
|
def pytest_configure(config):
|
|
"""Configure custom markers."""
|
|
config.addinivalue_line(
|
|
"markers", "rag: mark test as RAG integration test (requires --provider flag)"
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True, scope="module")
|
|
async def reset_all_singletons():
|
|
"""Reset ALL global singletons between test modules.
|
|
|
|
Prevents anyio.WouldBlock errors caused by stale singleton state
|
|
from previous test modules holding references to dead event loops
|
|
or closed memory streams.
|
|
"""
|
|
# Import all modules with singletons
|
|
import nextcloud_mcp_server.app as app_module
|
|
import nextcloud_mcp_server.auth.client_registry as client_registry_module
|
|
import nextcloud_mcp_server.embedding.service as embedding_module
|
|
import nextcloud_mcp_server.observability.tracing as tracing_module
|
|
import nextcloud_mcp_server.providers.registry as registry_module
|
|
import nextcloud_mcp_server.vector.qdrant_client as qdrant_module
|
|
|
|
# Store originals for restoration after test
|
|
originals = {
|
|
"qdrant_client": qdrant_module._qdrant_client,
|
|
"embedding_service": embedding_module._embedding_service,
|
|
"bm25_service": embedding_module._bm25_service,
|
|
"provider": registry_module._provider,
|
|
"vector_sync_state": (
|
|
app_module._vector_sync_state.document_send_stream,
|
|
app_module._vector_sync_state.document_receive_stream,
|
|
app_module._vector_sync_state.shutdown_event,
|
|
app_module._vector_sync_state.scanner_wake_event,
|
|
),
|
|
"tracer": tracing_module._tracer,
|
|
"registry": client_registry_module._registry,
|
|
}
|
|
|
|
# Close any open memory streams before reset
|
|
if app_module._vector_sync_state.document_send_stream is not None:
|
|
try:
|
|
await app_module._vector_sync_state.document_send_stream.aclose()
|
|
except Exception:
|
|
pass
|
|
if app_module._vector_sync_state.document_receive_stream is not None:
|
|
try:
|
|
await app_module._vector_sync_state.document_receive_stream.aclose()
|
|
except Exception:
|
|
pass
|
|
|
|
# Reset all singletons to None/fresh state
|
|
qdrant_module._qdrant_client = None
|
|
embedding_module._embedding_service = None
|
|
embedding_module._bm25_service = None
|
|
registry_module._provider = None
|
|
app_module._vector_sync_state.document_send_stream = None
|
|
app_module._vector_sync_state.document_receive_stream = None
|
|
app_module._vector_sync_state.shutdown_event = None
|
|
app_module._vector_sync_state.scanner_wake_event = None
|
|
tracing_module._tracer = None
|
|
client_registry_module._registry = None
|
|
|
|
logger.debug("All singletons reset for test module")
|
|
|
|
yield
|
|
|
|
# Cleanup: Close async resources created during test
|
|
if qdrant_module._qdrant_client is not None:
|
|
try:
|
|
await qdrant_module._qdrant_client.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# Restore originals
|
|
qdrant_module._qdrant_client = originals["qdrant_client"]
|
|
embedding_module._embedding_service = originals["embedding_service"]
|
|
embedding_module._bm25_service = originals["bm25_service"]
|
|
registry_module._provider = originals["provider"]
|
|
(
|
|
app_module._vector_sync_state.document_send_stream,
|
|
app_module._vector_sync_state.document_receive_stream,
|
|
app_module._vector_sync_state.shutdown_event,
|
|
app_module._vector_sync_state.scanner_wake_event,
|
|
) = originals["vector_sync_state"]
|
|
tracing_module._tracer = originals["tracer"]
|
|
client_registry_module._registry = originals["registry"]
|