from __future__ import annotations import os import pytest import pytest_asyncio from sqlalchemy import JSON, select from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine os.environ["DATABASE_URL"] = "postgresql+asyncpg://tester:tester@localhost:5432/pmflow_test" from app.models.base import Base from app.models.mounted_ai_runtime import ( MountedAgentTrace, MountedConversationRun, MountedTopicWorkspace, ) from app.services.mounted_ai_runtime_write_service import MountedAIRuntimeWriteService def _use_sqlite_json_types() -> None: for table in ( MountedTopicWorkspace.__table__, MountedConversationRun.__table__, MountedAgentTrace.__table__, ): for column in table.columns: if column.name in { "facts_json", "goals_json", "constraints_json", "options_json", "open_questions", "pending_actions", "bound_entities", "metadata_json", "context_layers_hit", "tool_calls", "execution_outcome", "input_summary", "output_summary", "tool_trace", }: column.type = JSON() @pytest_asyncio.fixture async def db_session(): engine = create_async_engine("sqlite+aiosqlite:///:memory:") _use_sqlite_json_types() async with engine.begin() as conn: await conn.run_sync( lambda sync_conn: Base.metadata.create_all( sync_conn, tables=[ MountedTopicWorkspace.__table__, MountedConversationRun.__table__, MountedAgentTrace.__table__, ], ) ) maker = async_sessionmaker(engine, expire_on_commit=False) async with maker() as session: yield session await engine.dispose() @pytest.mark.asyncio async def test_begin_ingress_run_uses_thread_anchor_and_sets_runtime_context(db_session) -> None: service = MountedAIRuntimeWriteService(db_session) tracking = await service.begin_ingress_run( tenant_id=1, source_system="feishu", source_message_id="om_message_1", message_text="帮我复盘一下这个直播需求。", sender="林宇航", channel_ref="chat_1", thread_context={ "thread_id": "thread_1", "root_message_id": "om_root_1", "reply_to_message_id": "om_reply_1", }, ) await db_session.commit() saved_topic = ( await db_session.execute(select(MountedTopicWorkspace)) ).scalar_one() saved_run = ( await db_session.execute(select(MountedConversationRun)) ).scalar_one() assert tracking.topic_workspace.id == saved_topic.id assert saved_topic.topic_key == "feishu:chat_1:root:om_root_1" assert saved_topic.thread_key == "thread_1" assert saved_topic.last_message_id == "om_message_1" assert saved_run.status == "running" assert saved_run.mode_before is None assert saved_run.context_layers_hit == { "current_message": True, "thread": True, "topic": False, } @pytest.mark.asyncio async def test_record_decision_and_finalize_run_persist_topic_mode_and_traces(db_session) -> None: service = MountedAIRuntimeWriteService(db_session) tracking = await service.begin_ingress_run( tenant_id=1, source_system="feishu", source_message_id="om_message_2", message_text="分配给谁做比较合适?", sender="林宇航", channel_ref="chat_2", thread_context={"root_message_id": "om_root_2"}, ) await service.record_orchestrator_decision( tracking=tracking, workflow_route="project_signal.create", intent_name="signal_create", intent_confidence=0.82, trigger_reason="manual_review", auto_execute=False, work_item_id=321, route_metadata={"workflow_family": "project_signal", "workflow_action": "create"}, ) await service.finalize_run( tracking=tracking, result={ "handled": True, "status": "captured", "classification": "command", "work_item_id": 321, "workflow_route": "project_signal.create", "direct_reply_text": "我先帮你收敛需求,再给负责人建议。", }, ) await db_session.commit() saved_topic = ( await db_session.execute(select(MountedTopicWorkspace)) ).scalar_one() saved_run = ( await db_session.execute(select(MountedConversationRun)) ).scalar_one() traces = ( await db_session.execute( select(MountedAgentTrace).order_by(MountedAgentTrace.id.asc()) ) ).scalars().all() assert saved_topic.mode == "plan" assert saved_topic.metadata_json["last_workflow_route"] == "project_signal.create" assert saved_run.mode_after == "plan" assert saved_run.status == "completed" assert saved_run.final_reply == "我先帮你收敛需求,再给负责人建议。" assert len(traces) == 2 assert traces[0].agent_name == "ConversationOrchestrator" assert traces[1].agent_name == "ReplyComposer"