105 findings to zero. Most were mechanical — 67 unused imports, and assorted
f-strings without placeholders. Three groups needed a decision.
The 15 F821 "undefined name" were forward references, not runtime errors. Each
annotation is quoted — `-> "WikiService"`, `Optional["IngestionService"]` — with
the real import inside the function body to break an import cycle. A quoted
annotation is never evaluated, so the code ran; the names were simply
unresolvable to any checker. They now have a TYPE_CHECKING block, which costs
nothing at import time and keeps the cycle broken.
The 6 E402 split two ways. `import secrets`, `Security`, `Request` and
`HTTPBearer` in dependencies.py had drifted below several hundred lines of
factory functions for no reason — stdlib and fastapi, no cycle to avoid — and
moved up. The other three are deliberate and now say so: the VectorService and
GraphService aliases import back into dependencies.py, and main.py's routers
expect a configured app, so both must stay put.
Bare `except:` narrowed to `except Exception:` in three places, which stops them
swallowing KeyboardInterrupt and SystemExit.
The 5 unused locals were all genuinely dead. One is worth naming rather than
fixing: qdrant_client.delete()'s return value was bound and never read, so a
failed delete is indistinguishable from a successful one — the assignment is
gone, but nothing checks the status either way and that has not changed here.
`timing = {}` in _retrieve_parallel looked like it might mean the reported
per-leg timings were always zero; traced, and they come from output["timing"],
so the local was only vestigial.
426 passed, 29 skipped, unchanged. The app imports and the service aliases still
resolve, which is the check that mattered after moving imports in
dependencies.py.
The gate still prints "not gated here yet: test (T-56)" — lint is green, tests
remain unwired, and that is left visible rather than silently absent.
Co-Authored-By: Claude <noreply@anthropic.com>
251 lines
7.0 KiB
Python
251 lines
7.0 KiB
Python
"""
|
|
Tests for GraphService - knowledge graph operations.
|
|
|
|
Tests cover:
|
|
- Document node creation with tags
|
|
- Entity-stub page skipping
|
|
- Entity extraction
|
|
|
|
Run with: pytest tests/test_graph_service.py -v -s
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock
|
|
|
|
from src.services.graph_service import GraphService
|
|
|
|
|
|
# Test constants
|
|
TEST_USER = "graph-tester"
|
|
TEST_PAGE_ID = 123
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_neo4j():
|
|
"""Mock Neo4j client."""
|
|
mock = AsyncMock()
|
|
mock.execute_query = AsyncMock(return_value=[{"d": {"page_id": TEST_PAGE_ID}}])
|
|
mock.execute_write = AsyncMock(return_value=[{"d": {"page_id": TEST_PAGE_ID}}])
|
|
return mock
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_wiki():
|
|
"""Mock Wiki.js client."""
|
|
mock = AsyncMock()
|
|
return mock
|
|
|
|
|
|
@pytest.fixture
|
|
def graph_service(mock_neo4j, mock_wiki):
|
|
"""Get GraphService with mocked dependencies."""
|
|
return GraphService(
|
|
neo4j_client=mock_neo4j,
|
|
wikijs_client=mock_wiki
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_page():
|
|
"""Sample wiki page data."""
|
|
return {
|
|
"id": TEST_PAGE_ID,
|
|
"title": "Test Page",
|
|
"path": f"users/{TEST_USER}/technology/docker",
|
|
"content": "Docker is a containerization platform. It uses containers to run applications.",
|
|
"tags": ["technology", "docker", "containers"]
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_page_without_tags():
|
|
"""Sample wiki page without tags."""
|
|
return {
|
|
"id": TEST_PAGE_ID,
|
|
"title": "Test Page No Tags",
|
|
"path": f"users/{TEST_USER}/misc/test",
|
|
"content": "This is a test page with no tags.",
|
|
"tags": []
|
|
}
|
|
|
|
|
|
class TestDocumentNodeCreation:
|
|
"""Test Document node creation in Neo4j."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_document_node_includes_tags(
|
|
self,
|
|
graph_service,
|
|
mock_neo4j,
|
|
mock_wiki,
|
|
sample_page
|
|
):
|
|
"""Test that Document node is created with tags property."""
|
|
mock_wiki.get_page = AsyncMock(return_value=sample_page)
|
|
|
|
result = await graph_service.update_from_page(
|
|
page_id=TEST_PAGE_ID,
|
|
user=TEST_USER
|
|
)
|
|
|
|
# Verify the write transaction was used
|
|
assert mock_neo4j.execute_write.called
|
|
assert result.success is True
|
|
|
|
# Find the document creation query
|
|
calls = mock_neo4j.execute_write.call_args_list
|
|
doc_creation_call = None
|
|
for call in calls:
|
|
query = call[0][0] if call[0] else ""
|
|
if "MERGE" in query and "Document" in query and "tags" in query:
|
|
doc_creation_call = call
|
|
break
|
|
|
|
assert doc_creation_call is not None, "Document creation query with tags not found"
|
|
|
|
# Verify tags are in the query parameters
|
|
params = doc_creation_call[0][1] if len(doc_creation_call[0]) > 1 else {}
|
|
assert "tags" in params
|
|
assert params["tags"] == ["technology", "docker", "containers"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_document_node_with_empty_tags(
|
|
self,
|
|
graph_service,
|
|
mock_neo4j,
|
|
mock_wiki,
|
|
sample_page_without_tags
|
|
):
|
|
"""Test Document node creation with empty tags list."""
|
|
mock_wiki.get_page = AsyncMock(return_value=sample_page_without_tags)
|
|
|
|
result = await graph_service.update_from_page(
|
|
page_id=TEST_PAGE_ID,
|
|
user=TEST_USER
|
|
)
|
|
|
|
assert result.success is True
|
|
|
|
# Find the document creation query
|
|
calls = mock_neo4j.execute_write.call_args_list
|
|
doc_creation_call = None
|
|
for call in calls:
|
|
query = call[0][0] if call[0] else ""
|
|
if "MERGE" in query and "Document" in query:
|
|
doc_creation_call = call
|
|
break
|
|
|
|
assert doc_creation_call is not None
|
|
params = doc_creation_call[0][1] if len(doc_creation_call[0]) > 1 else {}
|
|
assert "tags" in params
|
|
assert params["tags"] == []
|
|
|
|
|
|
class TestEntityStubSkipping:
|
|
"""Test that entity-stub pages are skipped."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_entity_stub_pages(
|
|
self,
|
|
graph_service,
|
|
mock_neo4j,
|
|
mock_wiki
|
|
):
|
|
"""Test that entity-stub tagged pages skip entity extraction."""
|
|
stub_page = {
|
|
"id": TEST_PAGE_ID,
|
|
"title": "Auto Entity",
|
|
"path": f"users/{TEST_USER}/entities/test",
|
|
"content": "Auto-generated content.",
|
|
"tags": ["entity-stub", "auto-generated"]
|
|
}
|
|
mock_wiki.get_page = AsyncMock(return_value=stub_page)
|
|
|
|
result = await graph_service.update_from_page(
|
|
page_id=TEST_PAGE_ID,
|
|
user=TEST_USER
|
|
)
|
|
|
|
# Should return success but skip processing
|
|
assert result.success is True
|
|
# Neo4j should NOT be called for entity-stub pages
|
|
assert mock_neo4j.execute_query.call_count == 0
|
|
assert mock_neo4j.execute_write.call_count == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_auto_generated_pages(
|
|
self,
|
|
graph_service,
|
|
mock_neo4j,
|
|
mock_wiki
|
|
):
|
|
"""Test that auto-generated tagged pages skip entity extraction."""
|
|
auto_page = {
|
|
"id": TEST_PAGE_ID,
|
|
"title": "Auto Page",
|
|
"path": f"users/{TEST_USER}/auto/test",
|
|
"content": "Auto-generated content.",
|
|
"tags": ["auto-generated"]
|
|
}
|
|
mock_wiki.get_page = AsyncMock(return_value=auto_page)
|
|
|
|
result = await graph_service.update_from_page(
|
|
page_id=TEST_PAGE_ID,
|
|
user=TEST_USER
|
|
)
|
|
|
|
assert result.success is True
|
|
assert mock_neo4j.execute_query.call_count == 0
|
|
assert mock_neo4j.execute_write.call_count == 0
|
|
|
|
|
|
class TestPageNotFound:
|
|
"""Test handling of missing pages."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_page_not_found_returns_failure(
|
|
self,
|
|
graph_service,
|
|
mock_wiki
|
|
):
|
|
"""Test that missing page returns failure result."""
|
|
mock_wiki.get_page = AsyncMock(return_value=None)
|
|
|
|
result = await graph_service.update_from_page(
|
|
page_id=999,
|
|
user=TEST_USER
|
|
)
|
|
|
|
# The service catches the exception and returns a failed result
|
|
assert result.success is False
|
|
assert result.error_message is not None
|
|
assert "not found" in result.error_message.lower()
|
|
|
|
|
|
class TestEntityExtraction:
|
|
"""Test entity extraction from page content."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creates_document_and_entities(
|
|
self,
|
|
graph_service,
|
|
mock_neo4j,
|
|
mock_wiki,
|
|
sample_page
|
|
):
|
|
"""Test that document and entity nodes are created."""
|
|
mock_wiki.get_page = AsyncMock(return_value=sample_page)
|
|
|
|
result = await graph_service.update_from_page(
|
|
page_id=TEST_PAGE_ID,
|
|
user=TEST_USER
|
|
)
|
|
|
|
assert result.success is True
|
|
# Should have called neo4j at least once (for document node)
|
|
assert mock_neo4j.execute_write.called
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v", "-s"])
|