PMFlow/backend/tests/test_semantic_context_assem...

142 lines
5.0 KiB
Python

from __future__ import annotations
import os
import pytest
import pytest_asyncio
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.models.base import Base
from app.models.mounted_ai_runtime import (
MountedConversationRun,
MountedDraftWorkspace,
MountedTopicWorkspace,
)
from app.services.ai_pmo.semantic_context.assembler import SemanticContextAssembler
def _use_sqlite_json_types() -> None:
for table in (
MountedTopicWorkspace.__table__,
MountedDraftWorkspace.__table__,
MountedConversationRun.__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",
}:
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__,
],
)
)
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_context_assembler_builds_packet_from_runtime_state(db_session) -> None:
topic = MountedTopicWorkspace(
tenant_id=7,
topic_key="feishu:chat_semantic:root:om_root_semantic",
source_system="feishu",
channel_ref="chat_semantic",
conversation_scope_key="feishu:chat_semantic:ou_semantic",
thread_key="thread_semantic",
root_message_id="om_root_semantic",
mode="plan",
status="active",
plan_status="collecting",
facts_json={"title": "OpenCow 风格语义上下文迁移"},
goals_json=["统一语义入口"],
constraints_json={"must_not": ["db.write"]},
open_questions=["当前是不是新需求 topic"],
pending_actions=["continue_requirement_plan"],
readiness_score=0.4,
metadata_json={"last_message_preview": "这次先把语义上下文统一掉"},
)
db_session.add(topic)
await db_session.flush()
draft = MountedDraftWorkspace(
tenant_id=7,
topic_workspace_id=int(topic.id),
state="collecting",
draft_version=2,
requirement_card={"goal": "统一语义入口", "scope": "先做 shadow mode"},
draft_payload={"draft_text": "当前先保留 mounted runtime 作为底座"},
required_confirmations=["需求草稿需要人工确认后才允许正式创建"],
)
db_session.add(draft)
db_session.add(
MountedConversationRun(
tenant_id=7,
trace_id="mounted:feishu:prev",
topic_workspace_id=int(topic.id),
source_system="feishu",
source_message_id="om_prev",
mode_before="plan",
mode_after="plan",
active_agent="PlanCoach",
status="completed",
trigger_reason="shadow",
context_layers_hit={"topic": True},
execution_outcome={
"source_message_preview": "先别执行,先整理需求上下文",
"semantic_decision": {"mode_decision": "stay_plan"},
},
final_reply="我先把上下文和草稿整理出来。",
)
)
await db_session.commit()
packet = await SemanticContextAssembler(db_session).assemble(
tenant_id=7,
operator_user_id=99,
topic_workspace_id=int(topic.id),
latest_user_message="这轮继续按 OpenCow 风格整理语义上下文。",
)
assert packet.packet_version == "v1"
assert packet.topic.topic_key == "feishu:chat_semantic:root:om_root_semantic"
assert packet.topic.plan_status == "collecting"
assert packet.draft is not None
assert packet.draft.draft_version == 2
assert packet.mode_card.workflow_mode == "PLAN"
assert "draft.patch" in packet.mode_card.allowed_actions
assert "project.create" in packet.mode_card.forbidden_actions
assert packet.thread.recent_turns[0].role == "user"
assert packet.thread.recent_turns[-1].role == "assistant"
assert packet.thread.open_questions == ["当前是不是新需求 topic"]