fix(mcp-kb): add input validation, path security, and health checks

Security fixes from deep review:
- Add input validation patterns for project_id, agent_id, collection
- Add path traversal protection for source_path (reject .., null bytes)
- Add error codes (INTERNAL_ERROR) to generic exception handlers
- Handle FieldInfo objects in validation for test robustness

Performance fixes:
- Enable concurrent hybrid search with asyncio.gather

Health endpoint improvements:
- Check all dependencies (database, Redis, LLM Gateway)
- Return degraded/unhealthy status based on dependency health
- Updated tests for new health check response structure

All 139 tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-04 01:18:50 +01:00
parent cd7a9ccbdf
commit 6bb376a336
3 changed files with 250 additions and 14 deletions

View File

@@ -13,10 +13,10 @@ class TestHealthCheck:
@pytest.mark.asyncio
async def test_health_check_healthy(self):
"""Test health check when healthy."""
"""Test health check when all dependencies are connected."""
import server
# Create a proper async context manager mock
# Create a proper async context manager mock for database
mock_conn = AsyncMock()
mock_conn.fetchval = AsyncMock(return_value=1)
@@ -29,24 +29,75 @@ class TestHealthCheck:
mock_cm.__aexit__.return_value = None
mock_db.acquire.return_value = mock_cm
# Mock Redis
mock_redis = AsyncMock()
mock_redis.ping = AsyncMock(return_value=True)
# Mock HTTP client for LLM Gateway
mock_http_response = AsyncMock()
mock_http_response.status_code = 200
mock_http_client = AsyncMock()
mock_http_client.get = AsyncMock(return_value=mock_http_response)
# Mock embeddings with Redis and HTTP client
mock_embeddings = MagicMock()
mock_embeddings._redis = mock_redis
mock_embeddings._http_client = mock_http_client
server._database = mock_db
server._embeddings = mock_embeddings
result = await server.health_check()
assert result["status"] == "healthy"
assert result["service"] == "knowledge-base"
assert result["database"] == "connected"
assert result["dependencies"]["database"] == "connected"
assert result["dependencies"]["redis"] == "connected"
assert result["dependencies"]["llm_gateway"] == "connected"
@pytest.mark.asyncio
async def test_health_check_no_database(self):
"""Test health check without database."""
"""Test health check without database - should be unhealthy."""
import server
server._database = None
server._embeddings = None
result = await server.health_check()
assert result["database"] == "not initialized"
assert result["status"] == "unhealthy"
assert result["dependencies"]["database"] == "not initialized"
@pytest.mark.asyncio
async def test_health_check_degraded(self):
"""Test health check with database but no Redis - should be degraded."""
import server
# Create a proper async context manager mock for database
mock_conn = AsyncMock()
mock_conn.fetchval = AsyncMock(return_value=1)
mock_db = MagicMock()
mock_db._pool = MagicMock()
mock_cm = AsyncMock()
mock_cm.__aenter__.return_value = mock_conn
mock_cm.__aexit__.return_value = None
mock_db.acquire.return_value = mock_cm
# Mock embeddings without Redis
mock_embeddings = MagicMock()
mock_embeddings._redis = None
mock_embeddings._http_client = None
server._database = mock_db
server._embeddings = mock_embeddings
result = await server.health_check()
assert result["status"] == "degraded"
assert result["dependencies"]["database"] == "connected"
assert result["dependencies"]["redis"] == "not initialized"
class TestSearchKnowledgeTool: