321 lines
11 KiB
Python
321 lines
11 KiB
Python
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,
|
|
MountedSourceActivation,
|
|
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__,
|
|
MountedSourceActivation.__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__,
|
|
MountedSourceActivation.__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:
|
|
db_session.add(
|
|
MountedSourceActivation(
|
|
tenant_id=1,
|
|
source_system="feishu",
|
|
scope_type="chat",
|
|
scope_ref="chat_1",
|
|
status="active",
|
|
cutover_mode="mounted_only",
|
|
auto_reply_enabled=True,
|
|
)
|
|
)
|
|
await db_session.commit()
|
|
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,
|
|
"source_activation": True,
|
|
}
|
|
assert saved_topic.metadata_json["source_activation"]["cutover_mode"] == "mounted_only"
|
|
assert saved_run.trigger_reason == "ingress:feishu:mounted_only"
|
|
|
|
|
|
@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"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_begin_ingress_run_skips_unmatched_chat_when_source_activation_whitelist_exists(db_session) -> None:
|
|
db_session.add(
|
|
MountedSourceActivation(
|
|
tenant_id=1,
|
|
source_system="feishu",
|
|
scope_type="chat",
|
|
scope_ref="whitelisted_chat",
|
|
status="active",
|
|
cutover_mode="mounted_only",
|
|
auto_reply_enabled=True,
|
|
)
|
|
)
|
|
await db_session.commit()
|
|
|
|
service = MountedAIRuntimeWriteService(db_session)
|
|
tracking = await service.begin_ingress_run(
|
|
tenant_id=1,
|
|
source_system="feishu",
|
|
source_message_id="om_message_3",
|
|
message_text="这个 chat 不在白名单里。",
|
|
sender="林宇航",
|
|
channel_ref="non_whitelisted_chat",
|
|
thread_context={"root_message_id": "om_root_3"},
|
|
)
|
|
|
|
assert tracking is None
|
|
topic_rows = (await db_session.execute(select(MountedTopicWorkspace))).scalars().all()
|
|
run_rows = (await db_session.execute(select(MountedConversationRun))).scalars().all()
|
|
assert topic_rows == []
|
|
assert run_rows == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_begin_ingress_run_persists_conversation_scope_key_for_feishu(db_session) -> None:
|
|
service = MountedAIRuntimeWriteService(db_session)
|
|
|
|
tracking = await service.begin_ingress_run(
|
|
tenant_id=1,
|
|
source_system="feishu",
|
|
source_message_id="om_message_4",
|
|
message_text="想跟你聊一个新需求。",
|
|
sender="林宇航",
|
|
channel_ref="chat_scope_1",
|
|
thread_context=None,
|
|
source_sender_open_id="ou_scope_1",
|
|
)
|
|
await db_session.commit()
|
|
|
|
saved_topic = (
|
|
await db_session.execute(select(MountedTopicWorkspace))
|
|
).scalar_one()
|
|
|
|
assert tracking is not None
|
|
assert saved_topic.conversation_scope_key == "feishu:chat_scope_1:ou_scope_1"
|
|
assert saved_topic.plan_status == "idle"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_topic_plan_state_persists_explicit_plan_status_and_pending_actions(db_session) -> None:
|
|
service = MountedAIRuntimeWriteService(db_session)
|
|
|
|
tracking = await service.begin_ingress_run(
|
|
tenant_id=1,
|
|
source_system="feishu",
|
|
source_message_id="om_message_5",
|
|
message_text="先聊需求,不直接创建。",
|
|
sender="林宇航",
|
|
channel_ref="chat_plan_state",
|
|
thread_context=None,
|
|
source_sender_open_id="ou_plan_state",
|
|
)
|
|
|
|
await service.update_topic_plan_state(
|
|
tracking=tracking,
|
|
plan_status="awaiting_execute_confirm",
|
|
topic_status="active",
|
|
pending_actions=["confirm_requirement_draft"],
|
|
metadata={"plan_topic_action": "draft_ready"},
|
|
)
|
|
await db_session.commit()
|
|
|
|
saved_topic = (
|
|
await db_session.execute(select(MountedTopicWorkspace))
|
|
).scalar_one()
|
|
|
|
assert saved_topic.plan_status == "awaiting_execute_confirm"
|
|
assert saved_topic.pending_actions == ["confirm_requirement_draft"]
|
|
assert saved_topic.metadata_json["plan_topic_action"] == "draft_ready"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_record_semantic_shadow_merges_with_finalize_run_execution_outcome(db_session) -> None:
|
|
service = MountedAIRuntimeWriteService(db_session)
|
|
|
|
tracking = await service.begin_ingress_run(
|
|
tenant_id=1,
|
|
source_system="feishu",
|
|
source_message_id="om_message_6",
|
|
message_text="先继续整理语义上下文,不要直接执行。",
|
|
sender="林宇航",
|
|
channel_ref="chat_semantic_shadow",
|
|
thread_context={"root_message_id": "om_root_6"},
|
|
source_sender_open_id="ou_semantic_shadow",
|
|
)
|
|
|
|
await service.record_semantic_shadow(
|
|
tracking=tracking,
|
|
source_message_preview="先继续整理语义上下文,不要直接执行。",
|
|
semantic_packet_summary={"packet_version": "v1", "recent_turn_count": 2},
|
|
semantic_decision={"topic_relation": "continue_current_topic", "mode_decision": "stay_plan"},
|
|
semantic_gate_result={"accepted": True, "next_plan_status": "collecting"},
|
|
semantic_diff={"changed": True, "legacy_route": "query.answer"},
|
|
)
|
|
await service.finalize_run(
|
|
tracking=tracking,
|
|
result={
|
|
"handled": True,
|
|
"status": "planned",
|
|
"classification": "query",
|
|
"work_item_id": 654,
|
|
"workflow_route": "query.answer",
|
|
"direct_reply_text": "我先把语义上下文整理成 shadow 观测结果。",
|
|
},
|
|
)
|
|
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 saved_topic.plan_status == "collecting"
|
|
assert saved_run.execution_outcome["handled"] is True
|
|
assert saved_run.execution_outcome["semantic_packet_summary"]["packet_version"] == "v1"
|
|
assert saved_run.execution_outcome["semantic_decision"]["mode_decision"] == "stay_plan"
|
|
assert saved_run.execution_outcome["semantic_gate_result"]["next_plan_status"] == "collecting"
|
|
assert saved_run.execution_outcome["semantic_diff"]["legacy_route"] == "query.answer"
|
|
assert saved_run.execution_outcome["source_message_preview"] == "先继续整理语义上下文,不要直接执行。"
|