"""Tests for Phase H — Wiki plugin: models, schemas, plugin manifest. All tests use mocks — no real DB needed. """ from __future__ import annotations import uuid from unittest.mock import MagicMock import pytest class TestWikiPluginManifest: """Test the Wiki plugin manifest.""" def test_plugin_manifest_valid(self): """Wiki plugin manifest is valid.""" from app.plugins.builtins.wiki.plugin import WikiPlugin assert WikiPlugin.manifest.name == "wiki" assert WikiPlugin.manifest.version == "1.0.0" assert WikiPlugin.manifest.display_name == "Wiki" assert "wiki:read" in WikiPlugin.manifest.permissions assert "wiki:write" in WikiPlugin.manifest.permissions assert "wiki:delete" in WikiPlugin.manifest.permissions assert "wiki:admin" in WikiPlugin.manifest.permissions def test_plugin_has_routes(self): """Wiki plugin has routes defined.""" from app.plugins.builtins.wiki.plugin import WikiPlugin assert len(WikiPlugin.manifest.routes) > 0 assert WikiPlugin.manifest.routes[0].path == "/api/v1/wiki" def test_plugin_has_menu_item(self): """Wiki plugin has a menu item.""" from app.plugins.builtins.wiki.plugin import WikiPlugin assert len(WikiPlugin.manifest.menu_items) > 0 assert WikiPlugin.manifest.menu_items[0].path == "/wiki" def test_plugin_has_dependencies(self): """Wiki plugin depends on permissions plugin.""" from app.plugins.builtins.wiki.plugin import WikiPlugin assert "permissions" in WikiPlugin.manifest.dependencies class TestWikiRoutes: """Test the Wiki routes are properly defined.""" def test_routes_count(self): """Wiki plugin has 9 routes.""" from app.plugins.builtins.wiki.routes import router assert len(router.routes) == 9 def test_routes_have_correct_paths(self): """Wiki routes have correct API paths.""" from app.plugins.builtins.wiki.routes import router paths = {r.path for r in router.routes} assert "/api/v1/wiki/articles" in paths assert "/api/v1/wiki/articles/{article_id}" in paths assert "/api/v1/wiki/articles/{article_id}/versions" in paths assert "/api/v1/wiki/articles/{article_id}/versions/{version}/restore" in paths assert "/api/v1/wiki/categories" in paths class TestWikiSchemas: """Test the Wiki Pydantic schemas.""" def test_article_create_valid(self): """ArticleCreate accepts valid data.""" from app.plugins.builtins.wiki.schemas import ArticleCreate article = ArticleCreate(title="Test Article", slug="test-article") assert article.title == "Test Article" assert article.slug == "test-article" assert article.status == "draft" assert article.tags == [] def test_article_create_invalid_status(self): """ArticleCreate rejects invalid status.""" from app.plugins.builtins.wiki.schemas import ArticleCreate from pydantic import ValidationError with pytest.raises(ValidationError): ArticleCreate(title="Test", slug="test", status="invalid") def test_article_update_partial(self): """ArticleUpdate allows partial updates.""" from app.plugins.builtins.wiki.schemas import ArticleUpdate update = ArticleUpdate(title="Updated Title") assert update.title == "Updated Title" assert update.content is None def test_category_create_valid(self): """CategoryCreate accepts valid data.""" from app.plugins.builtins.wiki.schemas import CategoryCreate cat = CategoryCreate(name="Test Category", slug="test-category") assert cat.name == "Test Category" assert cat.slug == "test-category" assert cat.sort_order == 0 class TestWikiModels: """Test the Wiki SQLAlchemy models.""" def test_wiki_article_model_fields(self): """WikiArticle model has all required fields.""" from app.plugins.builtins.wiki.models import WikiArticle assert hasattr(WikiArticle, "title") assert hasattr(WikiArticle, "slug") assert hasattr(WikiArticle, "content") assert hasattr(WikiArticle, "content_html") assert hasattr(WikiArticle, "summary") assert hasattr(WikiArticle, "category_id") assert hasattr(WikiArticle, "tags") assert hasattr(WikiArticle, "status") assert hasattr(WikiArticle, "entity_links") assert hasattr(WikiArticle, "version") assert hasattr(WikiArticle, "published_at") assert hasattr(WikiArticle, "tenant_id") assert hasattr(WikiArticle, "owner_id") def test_wiki_category_model_fields(self): """WikiCategory model has all required fields.""" from app.plugins.builtins.wiki.models import WikiCategory assert hasattr(WikiCategory, "name") assert hasattr(WikiCategory, "slug") assert hasattr(WikiCategory, "description") assert hasattr(WikiCategory, "parent_id") assert hasattr(WikiCategory, "sort_order") assert hasattr(WikiCategory, "tenant_id") def test_wiki_article_version_model_fields(self): """WikiArticleVersion model has all required fields.""" from app.plugins.builtins.wiki.models import WikiArticleVersion assert hasattr(WikiArticleVersion, "article_id") assert hasattr(WikiArticleVersion, "version") assert hasattr(WikiArticleVersion, "title") assert hasattr(WikiArticleVersion, "content") assert hasattr(WikiArticleVersion, "edited_by") assert hasattr(WikiArticleVersion, "edit_comment") assert hasattr(WikiArticleVersion, "tenant_id") def test_models_have_tenant_mixin(self): """All Wiki models have tenant_id (TenantMixin).""" from app.plugins.builtins.wiki.models import WikiArticle, WikiCategory, WikiArticleVersion for model in [WikiArticle, WikiCategory, WikiArticleVersion]: assert hasattr(model, "tenant_id") class TestWikiServices: """Test the Wiki service helper functions.""" def test_slugify(self): """_slugify converts text to URL-safe slug.""" from app.plugins.builtins.wiki.services import _slugify assert _slugify("Hello World") == "hello-world" assert _slugify("Überblick") == "ueberblick" assert _slugify("Test Article 123") == "test-article-123" # ─── H-SRC: Knowledge Source Adapter ───────────────────────────────────────── class TestKnowledgeSourceAdapter: """Test the knowledge source adapter (H-SRC).""" def test_available_sources(self): """All 4 knowledge source types are registered.""" from app.ai.knowledge_sources import get_available_sources sources = get_available_sources() types = {s["type"] for s in sources} assert types == {"wiki", "dms", "mail", "communication"} def test_get_source_config_wiki(self): """Wiki source config is correct.""" from app.ai.knowledge_sources import get_source_config config = get_source_config("wiki") assert config is not None assert config["text_field"] == "content" assert config["title_field"] == "title" assert config["status_filter"] == {"status": "published"} def test_get_source_config_unknown(self): """Unknown source type returns None.""" from app.ai.knowledge_sources import get_source_config assert get_source_config("nonexistent") is None # ─── H-CITE: Evidence References ───────────────────────────────────────────── class TestEvidenceReferences: """Test the evidence reference builder (H-CITE).""" def test_evidence_reference_to_dict(self): """EvidenceReference serializes to dict correctly.""" from app.ai.knowledge_sources import EvidenceReference ref = EvidenceReference( source_type="wiki", source_id="abc-123", title="Test Article", url="/wiki/articles/abc-123", snippet="This is a test snippet", confidence=0.95, ) d = ref.to_dict() assert d["source_type"] == "wiki" assert d["source_id"] == "abc-123" assert d["title"] == "Test Article" assert d["confidence"] == 0.95 def test_evidence_reference_to_workstream_block(self): """EvidenceReference converts to workstream block.""" from app.ai.knowledge_sources import EvidenceReference ref = EvidenceReference( source_type="dms", source_id="file-123", title="Document.pdf", url="/dms/files/file-123", snippet="Important content here", ) block = ref.to_workstream_block() assert block["type"] == "evidence_card" assert block["source_type"] == "dms" assert block["title"] == "Document.pdf" def test_build_evidence_references_from_results(self): """build_evidence_references creates refs from search results.""" from app.ai.knowledge_sources import build_evidence_references results = [ {"source_type": "wiki", "source_id": str(uuid.uuid4()), "title": "Article 1", "score": 0.9, "snippet": "Snippet 1"}, {"source_type": "dms", "source_id": str(uuid.uuid4()), "title": "Doc 2", "score": 0.7, "snippet": "Snippet 2"}, {"source_type": "mail", "source_id": str(uuid.uuid4()), "title": "Mail 3", "score": 0.95, "snippet": "Snippet 3"}, ] refs = build_evidence_references(results) assert len(refs) == 3 # Sorted by confidence descending assert refs[0].confidence == 0.95 assert refs[1].confidence == 0.9 assert refs[2].confidence == 0.7 def test_build_evidence_references_max_results(self): """build_evidence_references respects max_results.""" from app.ai.knowledge_sources import build_evidence_references results = [ {"source_type": "wiki", "source_id": str(uuid.uuid4()), "title": f"Article {i}", "score": 0.5} for i in range(10) ] refs = build_evidence_references(results, max_results=3) assert len(refs) == 3 def test_build_evidence_references_empty(self): """build_evidence_references handles empty results.""" from app.ai.knowledge_sources import build_evidence_references refs = build_evidence_references([]) assert refs == [] # ─── H-EXT/H-ENT/H-AUTO/H-CONF: Knowledge Extraction ───────────────────────── class TestKnowledgeExtraction: """Test the knowledge extraction module (H-EXT, H-ENT, H-AUTO, H-CONF).""" def test_extracted_entity_dataclass(self): """ExtractedEntity dataclass works correctly.""" from app.ai.knowledge_extraction import ExtractedEntity entity = ExtractedEntity(name="John Doe", entity_type="person", confidence=0.9) assert entity.name == "John Doe" assert entity.entity_type == "person" assert entity.confidence == 0.9 assert entity.mentions == [] assert entity.metadata == {} def test_extracted_relationship_dataclass(self): """ExtractedRelationship dataclass works correctly.""" from app.ai.knowledge_extraction import ExtractedRelationship rel = ExtractedRelationship( source_entity="John Doe", source_type="person", target_entity="Acme Corp", target_type="company", relationship_type="works_for", confidence=0.85, evidence="John works at Acme", ) assert rel.source_entity == "John Doe" assert rel.relationship_type == "works_for" assert rel.confidence == 0.85 assert rel.evidence == "John works at Acme" def test_extraction_result_dataclass(self): """ExtractionResult dataclass works correctly.""" from app.ai.knowledge_extraction import ExtractionResult result = ExtractionResult(source_type="wiki", source_id="abc", tenant_id="123") assert result.entities == [] assert result.relationships == [] assert result.overall_confidence == 0.0 def test_is_low_confidence(self): """is_low_confidence correctly identifies low confidence scores.""" from app.ai.knowledge_extraction import is_low_confidence, LOW_CONFIDENCE_THRESHOLD assert is_low_confidence(0.3) is True assert is_low_confidence(0.5) is True assert is_low_confidence(0.59) is True assert is_low_confidence(0.6) is False assert is_low_confidence(0.9) is False assert LOW_CONFIDENCE_THRESHOLD == 0.6 def test_filter_high_confidence(self): """filter_high_confidence splits relationships correctly.""" from app.ai.knowledge_extraction import filter_high_confidence, ExtractedRelationship rels = [ ExtractedRelationship("A", "person", "B", "company", "works_for", 0.9), ExtractedRelationship("C", "person", "D", "company", "related_to", 0.3), ExtractedRelationship("E", "person", "F", "company", "knows", 0.7), ExtractedRelationship("G", "person", "H", "company", "met", 0.5), ] high, low = filter_high_confidence(rels) assert len(high) == 2 # 0.9 and 0.7 assert len(low) == 2 # 0.3 and 0.5 assert high[0].confidence == 0.9 assert high[1].confidence == 0.7 def test_filter_high_confidence_custom_threshold(self): """filter_high_confidence respects custom threshold.""" from app.ai.knowledge_extraction import filter_high_confidence, ExtractedRelationship rels = [ ExtractedRelationship("A", "person", "B", "company", "works_for", 0.8), ExtractedRelationship("C", "person", "D", "company", "related_to", 0.7), ] high, low = filter_high_confidence(rels, threshold=0.75) assert len(high) == 1 # 0.8 assert len(low) == 1 # 0.7 @pytest.mark.asyncio async def test_extract_knowledge_empty_text(self): """extract_knowledge returns empty result for empty text.""" from app.ai.knowledge_extraction import extract_knowledge result = await extract_knowledge("", uuid.uuid4()) assert result.entities == [] assert result.relationships == [] assert result.overall_confidence == 0.0 @pytest.mark.asyncio async def test_extract_knowledge_short_text(self): """extract_knowledge returns empty result for very short text.""" from app.ai.knowledge_extraction import extract_knowledge result = await extract_knowledge("Hi", uuid.uuid4()) assert result.entities == [] assert result.relationships == [] # ─── H-EVT/H-DATA-LIFE/H-RET/H-ASK/H-REV: Knowledge Lifecycle ──────────────── class TestKnowledgeLifecycle: """Test the knowledge lifecycle module (H-EVT, H-DATA-LIFE, H-RET, H-ASK, H-REV).""" def test_should_extract_known_events(self): """should_extract returns True for configured extraction triggers.""" from app.ai.knowledge_lifecycle import should_extract assert should_extract("mail.received") is True assert should_extract("dms.file_uploaded") is True assert should_extract("wiki.article_published") is True assert should_extract("communication.message_created") is True def test_should_extract_unknown_events(self): """should_extract returns False for non-configured events.""" from app.ai.knowledge_lifecycle import should_extract assert should_extract("contact.created") is False assert should_extract("random.event") is False assert should_extract("") is False def test_extraction_triggers_count(self): """4 extraction triggers are configured.""" from app.ai.knowledge_lifecycle import EXTRACTION_TRIGGERS assert len(EXTRACTION_TRIGGERS) == 4 def test_retention_days_wiki_unlimited(self): """Wiki retention is 0 (unlimited).""" from app.ai.knowledge_lifecycle import get_retention_days assert get_retention_days("wiki") == 0 def test_retention_days_dms_one_year(self): """DMS retention is 365 days.""" from app.ai.knowledge_lifecycle import get_retention_days assert get_retention_days("dms") == 365 def test_retention_days_mail_180(self): """Mail retention is 180 days.""" from app.ai.knowledge_lifecycle import get_retention_days assert get_retention_days("mail") == 180 def test_retention_days_communication_90(self): """Communication retention is 90 days.""" from app.ai.knowledge_lifecycle import get_retention_days assert get_retention_days("communication") == 90 def test_retention_days_unknown_default(self): """Unknown source type gets default retention of 180 days.""" from app.ai.knowledge_lifecycle import get_retention_days assert get_retention_days("unknown") == 180 @pytest.mark.asyncio async def test_handle_extraction_event_unknown_event(self): """handle_extraction_event returns None for unknown events.""" from app.ai.knowledge_lifecycle import handle_extraction_event result = await handle_extraction_event( db=MagicMock(), tenant_id=uuid.uuid4(), event_name="unknown.event", payload={"entity_id": str(uuid.uuid4())}, ) assert result is None @pytest.mark.asyncio async def test_handle_extraction_event_no_entity_id(self): """handle_extraction_event returns None when no entity_id in payload.""" from app.ai.knowledge_lifecycle import handle_extraction_event result = await handle_extraction_event( db=MagicMock(), tenant_id=uuid.uuid4(), event_name="mail.received", payload={}, ) assert result is None @pytest.mark.asyncio async def test_ask_knowledge_empty_query(self): """ask_knowledge returns empty result for empty query.""" from app.ai.knowledge_lifecycle import ask_knowledge result = await ask_knowledge( db=MagicMock(), tenant_id=uuid.uuid4(), user_id=uuid.uuid4(), query="", ) assert result["answer"] == "" assert result["evidence"] == []