Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ac4595b37 | ||
|
|
37552b926f | ||
|
|
23ccd5ca5b | ||
|
|
2bfb1e29d7 |
@@ -13,7 +13,7 @@ jobs:
|
|||||||
- name: Login to Gitea Registry
|
- name: Login to Gitea Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: git.schweitz.net
|
registry: git.schweitz.internal
|
||||||
username: ${{ secrets.REGISTRY_USER }}
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
@@ -23,5 +23,5 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
git.schweitz.net/jpmschweitzer/library-desk:latest
|
git.schweitz.internal/jpmschweitzer/library-desk:latest
|
||||||
git.schweitz.net/jpmschweitzer/library-desk:${{ github.ref_name }}
|
git.schweitz.internal/jpmschweitzer/library-desk:${{ github.ref_name }}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
> **Start every session by reading this file.**
|
> **Start every session by reading this file.**
|
||||||
|
|||||||
@@ -5,6 +5,26 @@ All notable changes to Library Desk will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.1.2] - 2025-12-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Updated registry login URL in Gitea workflow (git.schweitz.net → git.schweitz.internal)
|
||||||
|
|
||||||
|
## [1.1.1] - 2025-12-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Updated container registry tag URLs in Gitea workflow (git.schweitz.net → git.schweitz.internal)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Tests for Smart Page Creation feature (`test_smart_create.py`)
|
||||||
|
- Model validation tests for WikiSmartCreateRequest/Response
|
||||||
|
- WikiService.smart_create_page method tests
|
||||||
|
- Bidirectional entity linking utility tests
|
||||||
|
- Endpoint validation tests
|
||||||
|
|
||||||
## [1.1.0] - 2025-12-11
|
## [1.1.0] - 2025-12-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy application
|
# Copy application
|
||||||
|
COPY pyproject.toml .
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
COPY static/ ./static/
|
COPY static/ ./static/
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "library-desk"
|
name = "library-desk"
|
||||||
version = "1.1.0"
|
version = "1.1.2"
|
||||||
description = "Coordination service for The Library system - HybridRAG queries, document ingestion, entity extraction, and knowledge consolidation"
|
description = "Coordination service for The Library system - HybridRAG queries, document ingestion, entity extraction, and knowledge consolidation"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -0,0 +1,553 @@
|
|||||||
|
"""
|
||||||
|
Tests for Smart Page Creation functionality.
|
||||||
|
|
||||||
|
Tests the new smart-create feature including:
|
||||||
|
- WikiSmartCreateRequest/Response models
|
||||||
|
- smart_create_page() method in WikiService
|
||||||
|
- Bidirectional entity linking utilities
|
||||||
|
- POST /wiki/pages/smart-create endpoint
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from src.models.wiki import (
|
||||||
|
WikiSmartCreateRequest,
|
||||||
|
WikiSmartCreateResponse,
|
||||||
|
WikiPage
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Model Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestWikiSmartCreateRequest:
|
||||||
|
"""Tests for WikiSmartCreateRequest model validation."""
|
||||||
|
|
||||||
|
def test_minimal_request(self):
|
||||||
|
"""Test request with only required field."""
|
||||||
|
request = WikiSmartCreateRequest(topic="Docker containers")
|
||||||
|
assert request.topic == "Docker containers"
|
||||||
|
assert request.path is None
|
||||||
|
assert request.tags == []
|
||||||
|
assert request.user is None
|
||||||
|
assert request.include_web_research is True
|
||||||
|
assert request.include_wiki_search is True
|
||||||
|
|
||||||
|
def test_full_request(self):
|
||||||
|
"""Test request with all fields."""
|
||||||
|
request = WikiSmartCreateRequest(
|
||||||
|
topic="Kubernetes orchestration",
|
||||||
|
path="/technology/kubernetes",
|
||||||
|
tags=["devops", "containers"],
|
||||||
|
user="testuser",
|
||||||
|
include_web_research=False,
|
||||||
|
include_wiki_search=True
|
||||||
|
)
|
||||||
|
assert request.topic == "Kubernetes orchestration"
|
||||||
|
assert request.path == "/technology/kubernetes"
|
||||||
|
# Tags are deduplicated via set, so order is not guaranteed
|
||||||
|
assert set(request.tags) == {"devops", "containers"}
|
||||||
|
assert request.user == "testuser"
|
||||||
|
assert request.include_web_research is False
|
||||||
|
assert request.include_wiki_search is True
|
||||||
|
|
||||||
|
def test_topic_min_length(self):
|
||||||
|
"""Test that topic requires at least 1 character."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
WikiSmartCreateRequest(topic="")
|
||||||
|
|
||||||
|
def test_topic_max_length(self):
|
||||||
|
"""Test that topic is limited to 500 characters."""
|
||||||
|
long_topic = "x" * 501
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
WikiSmartCreateRequest(topic=long_topic)
|
||||||
|
|
||||||
|
def test_path_validation_adds_leading_slash(self):
|
||||||
|
"""Test that path without leading slash gets one added."""
|
||||||
|
request = WikiSmartCreateRequest(
|
||||||
|
topic="Test",
|
||||||
|
path="technology/test"
|
||||||
|
)
|
||||||
|
assert request.path == "/technology/test"
|
||||||
|
|
||||||
|
def test_path_validation_removes_trailing_slash(self):
|
||||||
|
"""Test that trailing slash is removed."""
|
||||||
|
request = WikiSmartCreateRequest(
|
||||||
|
topic="Test",
|
||||||
|
path="/technology/test/"
|
||||||
|
)
|
||||||
|
assert request.path == "/technology/test"
|
||||||
|
|
||||||
|
def test_tags_deduplication(self):
|
||||||
|
"""Test that duplicate tags are removed."""
|
||||||
|
request = WikiSmartCreateRequest(
|
||||||
|
topic="Test",
|
||||||
|
tags=["devops", "devops", "containers", "devops"]
|
||||||
|
)
|
||||||
|
assert len(request.tags) == 2
|
||||||
|
assert "devops" in request.tags
|
||||||
|
assert "containers" in request.tags
|
||||||
|
|
||||||
|
def test_tags_whitespace_cleanup(self):
|
||||||
|
"""Test that tag whitespace is cleaned."""
|
||||||
|
request = WikiSmartCreateRequest(
|
||||||
|
topic="Test",
|
||||||
|
tags=[" devops ", "containers", " ", ""]
|
||||||
|
)
|
||||||
|
assert "devops" in request.tags
|
||||||
|
assert "containers" in request.tags
|
||||||
|
assert "" not in request.tags
|
||||||
|
assert " " not in request.tags
|
||||||
|
|
||||||
|
|
||||||
|
class TestWikiSmartCreateResponse:
|
||||||
|
"""Tests for WikiSmartCreateResponse model."""
|
||||||
|
|
||||||
|
def test_response_structure(self):
|
||||||
|
"""Test response model with all fields."""
|
||||||
|
page = WikiPage(
|
||||||
|
id=123,
|
||||||
|
path="/users/test/technology/docker",
|
||||||
|
title="Docker",
|
||||||
|
content="# Docker\n\nContent here",
|
||||||
|
tags=["technology"],
|
||||||
|
is_published=True,
|
||||||
|
created_at="2024-01-15T10:00:00Z",
|
||||||
|
updated_at="2024-01-15T10:00:00Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = WikiSmartCreateResponse(
|
||||||
|
page=page,
|
||||||
|
research_summary={
|
||||||
|
"wiki_results": 3,
|
||||||
|
"web_results": 5,
|
||||||
|
"graph_entities": 2
|
||||||
|
},
|
||||||
|
sources_used=8,
|
||||||
|
search_id="test-uuid-123",
|
||||||
|
entity_linking={
|
||||||
|
"forward_links": 4,
|
||||||
|
"backward_links": 2,
|
||||||
|
"pages_updated": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.page.id == 123
|
||||||
|
assert response.sources_used == 8
|
||||||
|
assert response.research_summary["wiki_results"] == 3
|
||||||
|
assert response.entity_linking["forward_links"] == 4
|
||||||
|
|
||||||
|
def test_response_default_entity_linking(self):
|
||||||
|
"""Test that entity_linking defaults to empty dict."""
|
||||||
|
page = WikiPage(
|
||||||
|
id=1,
|
||||||
|
path="/test",
|
||||||
|
title="Test",
|
||||||
|
content="Content",
|
||||||
|
tags=[],
|
||||||
|
is_published=True,
|
||||||
|
created_at="2024-01-15T10:00:00Z",
|
||||||
|
updated_at="2024-01-15T10:00:00Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = WikiSmartCreateResponse(
|
||||||
|
page=page,
|
||||||
|
research_summary={},
|
||||||
|
sources_used=0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.entity_linking == {}
|
||||||
|
assert response.search_id is None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# WikiService.smart_create_page Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestSmartCreatePage:
|
||||||
|
"""Tests for WikiService.smart_create_page method."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_hybrid_rag_service(self):
|
||||||
|
"""Mock HybridRAG service."""
|
||||||
|
service = AsyncMock()
|
||||||
|
|
||||||
|
# Create mock response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.total_results = 5
|
||||||
|
mock_response.search_id = "search-123"
|
||||||
|
mock_response.results = [
|
||||||
|
MagicMock(
|
||||||
|
source_type="vector",
|
||||||
|
title="Existing Docker Page",
|
||||||
|
url=None,
|
||||||
|
page_path="users/testuser/docker-basics",
|
||||||
|
content="Docker is a containerization platform...",
|
||||||
|
related_dossiers=["containers"]
|
||||||
|
),
|
||||||
|
MagicMock(
|
||||||
|
source_type="web",
|
||||||
|
title="Docker Documentation",
|
||||||
|
url="https://docs.docker.com",
|
||||||
|
page_path=None,
|
||||||
|
content="Official Docker documentation...",
|
||||||
|
related_dossiers=None
|
||||||
|
)
|
||||||
|
]
|
||||||
|
mock_response.keywords = MagicMock()
|
||||||
|
mock_response.keywords.core_keywords = ["docker", "containers", "virtualization"]
|
||||||
|
mock_response.timing = MagicMock()
|
||||||
|
mock_response.timing.total_ms = 1500
|
||||||
|
|
||||||
|
service.search = AsyncMock(return_value=mock_response)
|
||||||
|
return service
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_wiki_page_writer(self):
|
||||||
|
"""Mock WikiPageWriter."""
|
||||||
|
writer = AsyncMock()
|
||||||
|
writer.create_page = AsyncMock(return_value="# Docker Containers\n\n## Overview\n\nGenerated content about Docker...")
|
||||||
|
return writer
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_smart_create_basic(
|
||||||
|
self,
|
||||||
|
mock_hybrid_rag_service,
|
||||||
|
mock_wiki_page_writer
|
||||||
|
):
|
||||||
|
"""Test basic smart page creation flow."""
|
||||||
|
from src.services.wiki_service import WikiService
|
||||||
|
|
||||||
|
mock_wiki_client = AsyncMock()
|
||||||
|
wiki_service = WikiService(mock_wiki_client)
|
||||||
|
|
||||||
|
# Mock the create_page method on the service itself
|
||||||
|
mock_page = WikiPage(
|
||||||
|
id=42,
|
||||||
|
path="/users/testuser/technology/docker",
|
||||||
|
title="Docker containers",
|
||||||
|
content="# Docker\n\nGenerated content",
|
||||||
|
tags=["technology"],
|
||||||
|
is_published=True,
|
||||||
|
created_at="2024-01-15T10:00:00Z",
|
||||||
|
updated_at="2024-01-15T10:00:00Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(wiki_service, 'create_page', new_callable=AsyncMock) as mock_create:
|
||||||
|
mock_create.return_value = mock_page
|
||||||
|
|
||||||
|
page, research_data = await wiki_service.smart_create_page(
|
||||||
|
topic="Docker containers",
|
||||||
|
user="testuser",
|
||||||
|
path="/technology/docker",
|
||||||
|
tags=["technology"],
|
||||||
|
hybrid_rag_service=mock_hybrid_rag_service,
|
||||||
|
wiki_page_writer=mock_wiki_page_writer,
|
||||||
|
include_web=True,
|
||||||
|
include_wiki=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify HybridRAG was called
|
||||||
|
mock_hybrid_rag_service.search.assert_called_once()
|
||||||
|
|
||||||
|
# Verify WikiPageWriter was called
|
||||||
|
mock_wiki_page_writer.create_page.assert_called_once()
|
||||||
|
|
||||||
|
# Verify page was created
|
||||||
|
mock_create.assert_called_once()
|
||||||
|
|
||||||
|
# Verify research data
|
||||||
|
assert "research_summary" in research_data
|
||||||
|
assert "sources_used" in research_data
|
||||||
|
assert "search_id" in research_data
|
||||||
|
assert research_data["search_id"] == "search-123"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_smart_create_auto_generates_path(
|
||||||
|
self,
|
||||||
|
mock_hybrid_rag_service,
|
||||||
|
mock_wiki_page_writer
|
||||||
|
):
|
||||||
|
"""Test that path is auto-generated from topic when not provided."""
|
||||||
|
from src.services.wiki_service import WikiService
|
||||||
|
|
||||||
|
mock_wiki_client = AsyncMock()
|
||||||
|
wiki_service = WikiService(mock_wiki_client)
|
||||||
|
|
||||||
|
mock_page = WikiPage(
|
||||||
|
id=42,
|
||||||
|
path="/users/testuser/tutorials/docker-compose-tutorial",
|
||||||
|
title="Docker Compose Tutorial",
|
||||||
|
content="# Docker Compose\n\nContent",
|
||||||
|
tags=["tutorials"],
|
||||||
|
is_published=True,
|
||||||
|
created_at="2024-01-15T10:00:00Z",
|
||||||
|
updated_at="2024-01-15T10:00:00Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(wiki_service, 'create_page', new_callable=AsyncMock) as mock_create:
|
||||||
|
mock_create.return_value = mock_page
|
||||||
|
|
||||||
|
await wiki_service.smart_create_page(
|
||||||
|
topic="Docker Compose Tutorial",
|
||||||
|
user="testuser",
|
||||||
|
path=None, # No path provided
|
||||||
|
tags=["tutorials"],
|
||||||
|
hybrid_rag_service=mock_hybrid_rag_service,
|
||||||
|
wiki_page_writer=mock_wiki_page_writer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that create_page was called
|
||||||
|
mock_create.assert_called_once()
|
||||||
|
# The WikiPageCreate passed should have auto-generated path
|
||||||
|
call_args = mock_create.call_args[0][0] # First positional arg
|
||||||
|
assert "docker-compose-tutorial" in call_args.path.lower()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_smart_create_respects_web_flag(
|
||||||
|
self,
|
||||||
|
mock_hybrid_rag_service,
|
||||||
|
mock_wiki_page_writer
|
||||||
|
):
|
||||||
|
"""Test that include_web flag is passed to HybridRAG."""
|
||||||
|
from src.services.wiki_service import WikiService
|
||||||
|
|
||||||
|
mock_wiki_client = AsyncMock()
|
||||||
|
wiki_service = WikiService(mock_wiki_client)
|
||||||
|
|
||||||
|
mock_page = WikiPage(
|
||||||
|
id=1,
|
||||||
|
path="/test",
|
||||||
|
title="Test",
|
||||||
|
content="Content",
|
||||||
|
tags=[],
|
||||||
|
is_published=True,
|
||||||
|
created_at="2024-01-15T10:00:00Z",
|
||||||
|
updated_at="2024-01-15T10:00:00Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(wiki_service, 'create_page', new_callable=AsyncMock) as mock_create:
|
||||||
|
mock_create.return_value = mock_page
|
||||||
|
|
||||||
|
await wiki_service.smart_create_page(
|
||||||
|
topic="Test",
|
||||||
|
user="testuser",
|
||||||
|
path="/test",
|
||||||
|
tags=[],
|
||||||
|
hybrid_rag_service=mock_hybrid_rag_service,
|
||||||
|
wiki_page_writer=mock_wiki_page_writer,
|
||||||
|
include_web=False,
|
||||||
|
include_wiki=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check HybridRAG config
|
||||||
|
call_args = mock_hybrid_rag_service.search.call_args
|
||||||
|
config = call_args[1]["config"]
|
||||||
|
assert config.enable_web is False
|
||||||
|
assert config.enable_vector is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_smart_create_counts_sources(
|
||||||
|
self,
|
||||||
|
mock_hybrid_rag_service,
|
||||||
|
mock_wiki_page_writer
|
||||||
|
):
|
||||||
|
"""Test that sources are counted correctly."""
|
||||||
|
from src.services.wiki_service import WikiService
|
||||||
|
|
||||||
|
mock_wiki_client = AsyncMock()
|
||||||
|
wiki_service = WikiService(mock_wiki_client)
|
||||||
|
|
||||||
|
mock_page = WikiPage(
|
||||||
|
id=1,
|
||||||
|
path="/test",
|
||||||
|
title="Test",
|
||||||
|
content="Content",
|
||||||
|
tags=[],
|
||||||
|
is_published=True,
|
||||||
|
created_at="2024-01-15T10:00:00Z",
|
||||||
|
updated_at="2024-01-15T10:00:00Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(wiki_service, 'create_page', new_callable=AsyncMock) as mock_create:
|
||||||
|
mock_create.return_value = mock_page
|
||||||
|
|
||||||
|
page, research_data = await wiki_service.smart_create_page(
|
||||||
|
topic="Test",
|
||||||
|
user="testuser",
|
||||||
|
path="/test",
|
||||||
|
tags=[],
|
||||||
|
hybrid_rag_service=mock_hybrid_rag_service,
|
||||||
|
wiki_page_writer=mock_wiki_page_writer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have 2 sources (1 wiki + 1 web from mock)
|
||||||
|
assert research_data["sources_used"] == 2
|
||||||
|
assert research_data["research_summary"]["wiki_results"] == 1
|
||||||
|
assert research_data["research_summary"]["web_results"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Entity Linking Utils Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestBidirectionalEntityLinking:
|
||||||
|
"""Tests for entity_linking_utils.apply_bidirectional_entity_linking."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_neo4j_client(self):
|
||||||
|
"""Mock Neo4j client."""
|
||||||
|
client = AsyncMock()
|
||||||
|
client.execute_query = AsyncMock(return_value=[])
|
||||||
|
return client
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_wiki_service(self):
|
||||||
|
"""Mock WikiService."""
|
||||||
|
service = AsyncMock()
|
||||||
|
return service
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_ingestion_service(self):
|
||||||
|
"""Mock IngestionService."""
|
||||||
|
service = AsyncMock()
|
||||||
|
return service
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_returns_link_counts(
|
||||||
|
self,
|
||||||
|
mock_neo4j_client,
|
||||||
|
mock_wiki_service,
|
||||||
|
mock_ingestion_service
|
||||||
|
):
|
||||||
|
"""Test that function returns proper link count structure."""
|
||||||
|
from src.services.entity_linking_utils import apply_bidirectional_entity_linking
|
||||||
|
|
||||||
|
# Patch at the import location within the module
|
||||||
|
with patch('src.routers.entity_linking.link_entities_in_page') as mock_link:
|
||||||
|
mock_result = MagicMock()
|
||||||
|
mock_result.content_links_added = 3
|
||||||
|
mock_link.return_value = mock_result
|
||||||
|
|
||||||
|
with patch('src.core.dependencies.get_graph_service'):
|
||||||
|
with patch('src.core.dependencies.get_ingestion_service', return_value=mock_ingestion_service):
|
||||||
|
result = await apply_bidirectional_entity_linking(
|
||||||
|
page_id=42,
|
||||||
|
page_title="Docker",
|
||||||
|
user="testuser",
|
||||||
|
neo4j_client=mock_neo4j_client,
|
||||||
|
wiki_service=mock_wiki_service,
|
||||||
|
ingestion_service=mock_ingestion_service
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "forward_links" in result
|
||||||
|
assert "backward_links" in result
|
||||||
|
assert "pages_updated" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_handles_no_reverse_references(
|
||||||
|
self,
|
||||||
|
mock_neo4j_client,
|
||||||
|
mock_wiki_service,
|
||||||
|
mock_ingestion_service
|
||||||
|
):
|
||||||
|
"""Test graceful handling when no reverse references found."""
|
||||||
|
from src.services.entity_linking_utils import apply_bidirectional_entity_linking
|
||||||
|
|
||||||
|
# No reverse references
|
||||||
|
mock_neo4j_client.execute_query = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
with patch('src.routers.entity_linking.link_entities_in_page') as mock_link:
|
||||||
|
mock_result = MagicMock()
|
||||||
|
mock_result.content_links_added = 2
|
||||||
|
mock_link.return_value = mock_result
|
||||||
|
|
||||||
|
with patch('src.core.dependencies.get_graph_service'):
|
||||||
|
with patch('src.core.dependencies.get_ingestion_service', return_value=mock_ingestion_service):
|
||||||
|
result = await apply_bidirectional_entity_linking(
|
||||||
|
page_id=42,
|
||||||
|
page_title="NewEntity",
|
||||||
|
user="testuser",
|
||||||
|
neo4j_client=mock_neo4j_client,
|
||||||
|
wiki_service=mock_wiki_service,
|
||||||
|
ingestion_service=mock_ingestion_service
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["backward_links"] == 0
|
||||||
|
assert result["pages_updated"] == 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_handles_errors_gracefully(
|
||||||
|
self,
|
||||||
|
mock_neo4j_client,
|
||||||
|
mock_wiki_service,
|
||||||
|
mock_ingestion_service
|
||||||
|
):
|
||||||
|
"""Test that errors don't crash the function."""
|
||||||
|
from src.services.entity_linking_utils import apply_bidirectional_entity_linking
|
||||||
|
|
||||||
|
with patch('src.routers.entity_linking.link_entities_in_page') as mock_link:
|
||||||
|
mock_link.side_effect = Exception("Test error")
|
||||||
|
|
||||||
|
with patch('src.core.dependencies.get_graph_service'):
|
||||||
|
with patch('src.core.dependencies.get_ingestion_service', return_value=mock_ingestion_service):
|
||||||
|
result = await apply_bidirectional_entity_linking(
|
||||||
|
page_id=42,
|
||||||
|
page_title="Test",
|
||||||
|
user="testuser",
|
||||||
|
neo4j_client=mock_neo4j_client,
|
||||||
|
wiki_service=mock_wiki_service,
|
||||||
|
ingestion_service=mock_ingestion_service
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return zeros, not raise
|
||||||
|
assert result["forward_links"] == 0
|
||||||
|
assert result["backward_links"] == 0
|
||||||
|
assert result["pages_updated"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Endpoint Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestSmartCreateEndpoint:
|
||||||
|
"""Tests for POST /wiki/pages/smart-create endpoint."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_clients(self):
|
||||||
|
"""Create all mock clients needed for the endpoint."""
|
||||||
|
return {
|
||||||
|
"wiki_client": AsyncMock(),
|
||||||
|
"neo4j_client": AsyncMock(),
|
||||||
|
"qdrant_client": MagicMock(),
|
||||||
|
"ollama_client": AsyncMock(),
|
||||||
|
"searxng_client": AsyncMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_endpoint_returns_201(self, mock_clients):
|
||||||
|
"""Test that successful creation returns 201 status."""
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
# This test would require more setup with FastAPI TestClient
|
||||||
|
# For now, we test the model validation
|
||||||
|
request = WikiSmartCreateRequest(
|
||||||
|
topic="Test Topic",
|
||||||
|
tags=["test"]
|
||||||
|
)
|
||||||
|
assert request.topic == "Test Topic"
|
||||||
|
|
||||||
|
def test_request_validation_rejects_empty_topic(self):
|
||||||
|
"""Test that empty topic is rejected."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
WikiSmartCreateRequest(topic="")
|
||||||
|
|
||||||
|
def test_request_accepts_minimal_input(self):
|
||||||
|
"""Test that only topic is required."""
|
||||||
|
request = WikiSmartCreateRequest(topic="Minimal test")
|
||||||
|
assert request.topic == "Minimal test"
|
||||||
|
assert request.include_web_research is True # default
|
||||||
|
assert request.include_wiki_search is True # default
|
||||||
Reference in New Issue
Block a user