from __future__ import annotations import pytest import pytest_asyncio from sqlalchemy import JSON, select from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from app.models.base import Base from app.models.ingest_event import IngestEvent from app.models.knowledge_base_doc import KnowledgeBaseDoc from app.models.project import Project from app.models.signal import Signal from app.models.task import Task from app.models.truth_node import TruthNode from app.models.user import User from app.services.semantic_fact_service.hooks import register_semantic_fact_hooks def _use_sqlite_json_types(*tables) -> None: for table in tables: for column in table.columns: if column.type.__class__.__name__ in {"JSONB", "ARRAY", "Vector"}: column.type = JSON() @pytest_asyncio.fixture async def db_session(): engine = create_async_engine("sqlite+aiosqlite:///:memory:") tables = [ User.__table__, Project.__table__, Signal.__table__, TruthNode.__table__, Task.__table__, KnowledgeBaseDoc.__table__, IngestEvent.__table__, ] _use_sqlite_json_types(*tables) async with engine.begin() as conn: await conn.run_sync(lambda sync_conn: Base.metadata.create_all(sync_conn, tables=tables)) maker = async_sessionmaker(engine, expire_on_commit=False) async with maker() as session: yield session await engine.dispose() @pytest.mark.asyncio async def test_semantic_fact_hooks_enqueue_extract_events_for_supported_entities(db_session) -> None: register_semantic_fact_hooks() user = User( tenant_id=1, username="semantic_admin", email="semantic_admin@example.com", password_hash="hash", name="管理员", role="ADMIN", status="active", is_active=True, ) db_session.add(user) await db_session.flush() project = Project( tenant_id=1, name="alpha", title="Alpha 项目", description="项目描述", owner_id=int(user.id), status="active", assigned_role="backend", ) signal = Signal( tenant_id=1, title="信号标题", content="信号内容", status="待处理", processed=False, priority="high", ) doc = KnowledgeBaseDoc( tenant_id=1, title="知识文档", file_type="md", source_type="file", status="complete", uploaded_by=int(user.id), ) db_session.add_all([project, signal, doc]) await db_session.flush() charter = TruthNode( tenant_id=1, node_type="charter", parent_id=None, project_id=int(project.id), signal_id=None, created_by=int(user.id), title="开票系统需求", content={"summary": "建设开票系统"}, version=1, status="draft", ) release = TruthNode( tenant_id=1, node_type="release", parent_id=None, project_id=int(project.id), signal_id=None, created_by=int(user.id), title="发布记录", content={"summary": "上线版本"}, version=1, status="pending", ) review = TruthNode( tenant_id=1, node_type="review", parent_id=None, project_id=int(project.id), signal_id=None, created_by=int(user.id), title="阶段复盘", content={"summary": "复盘内容"}, version=1, status="draft", ) db_session.add_all([charter, release, review]) await db_session.flush() task = Task( tenant_id=1, truth_node_id=int(charter.id), project_id=int(project.id), assignee_id=int(user.id), ai_recommended_assignees=None, ai_breakdown_payload=None, manual_overrides=None, title="实现开票接口", description="任务描述", required_skill_tag_ids=[], status="todo", priority="high", estimated_hours=None, actual_hours=None, quality_score=None, due_date=None, completed_at=None, ) db_session.add(task) await db_session.commit() rows = ( await db_session.execute( select(IngestEvent).where( IngestEvent.tenant_id == 1, IngestEvent.channel == "semantic_fact_extract", ).order_by(IngestEvent.event_key.asc()) ) ).scalars().all() payload_by_key = {str(row.event_key): row.payload for row in rows} assert payload_by_key["document:%s" % int(doc.id)]["operation"] == "extract" assert payload_by_key["project:%s" % int(project.id)]["operation"] == "extract" assert payload_by_key["release:%s" % int(release.id)]["entity_type"] == "release" assert payload_by_key["requirement:%s" % int(charter.id)]["entity_type"] == "requirement" assert payload_by_key["review:%s" % int(review.id)]["entity_type"] == "review" assert payload_by_key["signal:%s" % int(signal.id)]["entity_type"] == "signal" assert payload_by_key["task:%s" % int(task.id)]["entity_type"] == "task" assert payload_by_key["user:%s" % int(user.id)]["entity_type"] == "user" @pytest.mark.asyncio async def test_semantic_fact_hooks_switch_to_deactivate_for_archive_disable_and_delete(db_session) -> None: register_semantic_fact_hooks() user = User( tenant_id=1, username="semantic_admin_2", email="semantic_admin_2@example.com", password_hash="hash", name="管理员2", role="ADMIN", status="active", is_active=True, ) project = Project( tenant_id=1, name="alpha", title="Alpha 项目", description="项目描述", owner_id=None, status="active", assigned_role="backend", ) signal = Signal( tenant_id=1, title="信号标题", content="信号内容", status="待处理", processed=False, priority="high", ) doc = KnowledgeBaseDoc( tenant_id=1, title="知识文档", file_type="md", source_type="file", status="complete", uploaded_by=None, ) db_session.add_all([user, project, signal, doc]) await db_session.flush() review = TruthNode( tenant_id=1, node_type="review", parent_id=None, project_id=int(project.id), signal_id=None, created_by=int(user.id), title="阶段复盘", content={"summary": "复盘内容"}, version=1, status="draft", ) db_session.add(review) await db_session.commit() user.is_active = False user.status = "offline" signal.status = "已关闭" review.status = "archived" await db_session.delete(doc) await db_session.commit() rows = ( await db_session.execute( select(IngestEvent).where( IngestEvent.tenant_id == 1, IngestEvent.channel == "semantic_fact_extract", ) ) ).scalars().all() payload_by_key = {str(row.event_key): row.payload for row in rows} assert payload_by_key["document:%s" % int(doc.id)]["operation"] == "deactivate" assert payload_by_key["review:%s" % int(review.id)]["operation"] == "deactivate" assert payload_by_key["signal:%s" % int(signal.id)]["operation"] == "deactivate" assert payload_by_key["user:%s" % int(user.id)]["operation"] == "deactivate"