PMFlow/backend/tests/test_semantic_context_runti...

166 lines
5.6 KiB
Python

from __future__ import annotations
import os
from types import SimpleNamespace
import pytest
import pytest_asyncio
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy import JSON
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
os.environ["DATABASE_URL"] = "postgresql+asyncpg://tester:tester@localhost:5432/pmflow_test"
from app.api.ai_steward_api import routes_mounted_ai
from app.core.database import get_db
from app.dependencies.auth import get_current_user
from app.dependencies.tenant import get_tenant_id
from app.models.base import Base
from app.models.mounted_ai_runtime import (
MountedAgentTrace,
MountedConversationRun,
MountedDraftWorkspace,
MountedTopicWorkspace,
)
def _use_sqlite_json_types() -> None:
for table in (
MountedTopicWorkspace.__table__,
MountedDraftWorkspace.__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",
"requirement_card",
"draft_payload",
"required_confirmations",
"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__,
MountedDraftWorkspace.__table__,
MountedConversationRun.__table__,
MountedAgentTrace.__table__,
],
)
)
maker = async_sessionmaker(engine, expire_on_commit=False)
async with maker() as session:
yield session
await engine.dispose()
def _build_app(db_session):
app = FastAPI()
app.include_router(routes_mounted_ai.router)
async def _fake_get_db():
yield db_session
app.dependency_overrides[get_db] = _fake_get_db
app.dependency_overrides[get_tenant_id] = lambda: 1
app.dependency_overrides[get_current_user] = lambda: SimpleNamespace(
id=9001,
tenant_id=1,
role="ADMIN",
name="管理员",
username="admin",
)
return app
@pytest.mark.asyncio
async def test_mounted_runtime_run_detail_exposes_semantic_shadow_fields(db_session) -> None:
topic = MountedTopicWorkspace(
tenant_id=1,
topic_key="feishu:chat_1:root:om_root_1",
source_system="feishu",
channel_ref="chat_1",
thread_key="thread_1",
mode="plan",
status="active",
plan_status="collecting",
facts_json={"title": "语义上下文迁移"},
open_questions=["是否切新 topic"],
metadata_json={"last_message_preview": "这次继续统一语义入口"},
)
db_session.add(topic)
await db_session.flush()
draft = MountedDraftWorkspace(
tenant_id=1,
topic_workspace_id=int(topic.id),
state="collecting",
draft_version=3,
requirement_card={"goal": "统一语义入口"},
draft_payload={"draft_text": "先做 shadow mode"},
required_confirmations=["需求草稿需要人工确认后才允许正式创建"],
)
db_session.add(draft)
await db_session.flush()
run = MountedConversationRun(
tenant_id=1,
trace_id="mounted:feishu:semantic-detail",
topic_workspace_id=int(topic.id),
source_system="feishu",
source_message_id="om_message_1",
mode_before="plan",
mode_after="plan",
active_agent="ConversationOrchestrator",
status="completed",
trigger_reason="shadow",
context_layers_hit={"topic": True, "thread": True},
execution_outcome={
"source_message_preview": "这轮继续按 OpenCow 风格整理语义上下文",
"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"},
},
final_reply="我先把上下文整理出来。",
)
db_session.add(run)
await db_session.commit()
app = _build_app(db_session)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get(f"/ai-steward/mounted-ai/runs/{run.id}")
assert resp.status_code == 200
payload = resp.json()["data"]
assert payload["semantic_packet_summary"]["packet_version"] == "v1"
assert payload["semantic_decision"]["mode_decision"] == "stay_plan"
assert payload["semantic_gate_result"]["accepted"] is True
assert payload["semantic_diff"]["legacy_route"] == "query.answer"
assert payload["topic"]["topic_key"] == "feishu:chat_1:root:om_root_1"
assert payload["draft"]["draft_version"] == 3