PMFlow/backend/tests/test_semantic_fact_runtime_...

251 lines
7.4 KiB
Python

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="REVIEWER",
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="REVIEWER",
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"