Implements comprehensive service client layer for Library Desk API to support
Librarian AI agent with multi-tenant knowledge management across Neo4j, Qdrant,
Wiki.js, SearXNG, and Ollama.
## Service Clients (src/clients/)
- Neo4j async client with connection pooling and user-scoped labels
- Qdrant vector store with collection-per-user multi-tenancy
- Wiki.js GraphQL API client for page/dossier management
- SearXNG client for web search integration
- Ollama client for text embeddings (nomic-embed-text)
## Core Infrastructure (src/core/)
- Multi-tenancy helpers for user namespace management
- Wiki.js: path-based namespaces (/users/{user})
- Neo4j: user-specific labels (User_{User}_Document)
- Qdrant: collection per user (library_desk_{user})
- Dependency injection with FastAPI Depends and @lru_cache singletons
- Lifecycle management (startup/shutdown) for all service connections
## Background Jobs (src/jobs/)
- Redis-based job manager for long-running operations
- Job status tracking with 24-hour TTL
- Support for queued, processing, completed, failed states
## Configuration
- Updated config.py with Redis DB 4 for library-desk jobs
- Updated docker-compose.yml: REDIS_DB from 2 to 4
- Added pytest and pytest-asyncio to requirements.txt
## Testing
- Unit tests: 25/25 passed (multi-tenancy helpers)
- Integration tests: 12/12 passed (all services verified)
- Neo4j connection and CRUD operations
- Qdrant vector operations with 768-dim embeddings
- Wiki.js GraphQL queries
- SearXNG web search
- Job Manager with Redis
- Dependency injection lifecycle
- pytest.ini configuration with asyncio support
## Health Monitoring
- Real-time service health checks via /health endpoint
- Connection status for all 5 external services
- Graceful degradation for partial service availability
## Architecture
- Follows async/await pattern throughout
- Connection pooling for Neo4j (singleton driver)
- HTTP client lifecycle management (httpx)
- Multi-tenancy enforced at client layer
- Default user: jpmschweitzer
Files changed: 26 files
- 5 new service clients (~1500 lines)
- 2 core modules (~500 lines)
- 1 job manager (~350 lines)
- 3 test files with 37 test cases
- Updated main.py with lifecycle hooks
All services tested and operational. Ready for Phase 2 (routers/services).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
110 lines
2.3 KiB
Python
110 lines
2.3 KiB
Python
"""Pytest configuration and shared fixtures for Library Desk tests."""
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from typing import AsyncGenerator
|
|
|
|
# Test configuration
|
|
pytest_plugins = ("pytest_asyncio",)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_user() -> str:
|
|
"""Default test user."""
|
|
return "test_user"
|
|
|
|
|
|
@pytest.fixture
|
|
def neo4j_test_uri() -> str:
|
|
"""Test Neo4j URI."""
|
|
return "bolt://neo4j:7687"
|
|
|
|
|
|
@pytest.fixture
|
|
def neo4j_test_auth() -> tuple:
|
|
"""Test Neo4j authentication."""
|
|
return ("neo4j", "test_password")
|
|
|
|
|
|
@pytest.fixture
|
|
def qdrant_test_url() -> str:
|
|
"""Test Qdrant URL."""
|
|
return "http://qdrant:6333"
|
|
|
|
|
|
@pytest.fixture
|
|
def wikijs_test_config() -> dict:
|
|
"""Test Wiki.js configuration."""
|
|
return {
|
|
"base_url": "http://wiki:3000",
|
|
"api_key": "test_api_key"
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def searxng_test_url() -> str:
|
|
"""Test SearXNG URL."""
|
|
return "http://searxng:8080"
|
|
|
|
|
|
@pytest.fixture
|
|
def ollama_test_config() -> dict:
|
|
"""Test Ollama configuration."""
|
|
return {
|
|
"base_url": "http://ollama:11434",
|
|
"model": "nomic-embed-text"
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def redis_test_url() -> str:
|
|
"""Test Redis URL."""
|
|
return "redis://redis-shared:6379/4"
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_document() -> dict:
|
|
"""Sample document for testing."""
|
|
return {
|
|
"id": "test_doc_1",
|
|
"title": "Test Document",
|
|
"content": "This is a test document for unit testing.",
|
|
"metadata": {
|
|
"source": "test",
|
|
"author": "test_user"
|
|
}
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_chunks() -> list:
|
|
"""Sample document chunks for testing."""
|
|
return [
|
|
{
|
|
"content": "First chunk of text.",
|
|
"metadata": {"chunk_index": 0}
|
|
},
|
|
{
|
|
"content": "Second chunk of text.",
|
|
"metadata": {"chunk_index": 1}
|
|
},
|
|
{
|
|
"content": "Third chunk of text.",
|
|
"metadata": {"chunk_index": 2}
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_embeddings() -> list:
|
|
"""Sample embeddings for testing (768-dimensional for nomic-embed-text)."""
|
|
import random
|
|
random.seed(42) # Reproducible embeddings
|
|
|
|
# Generate 3 sample 768-dimensional embeddings
|
|
return [
|
|
[random.random() for _ in range(768)],
|
|
[random.random() for _ in range(768)],
|
|
[random.random() for _ in range(768)]
|
|
]
|