PMFlow/backend/tests/test_executive_briefing_api.py

229 lines
8.0 KiB
Python

from __future__ import annotations
from datetime import datetime
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
from app.api.ai_steward_api import routes_executive_briefing
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.integration import Integration
from app.services.executive_briefing_service import ExecutiveBriefingService
def _use_sqlite_json_types() -> None:
Integration.__table__.c.config.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=[
Integration.__table__,
],
)
)
maker = async_sessionmaker(engine, expire_on_commit=False)
async with maker() as session:
yield session
await engine.dispose()
def _build_app(*, db_session, role: str = "ADMIN") -> FastAPI:
app = FastAPI()
app.include_router(routes_executive_briefing.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,
name="管理员",
username="admin",
role=role,
)
return app
@pytest.mark.asyncio
async def test_executive_briefing_snapshot_and_live_are_enveloped(db_session, monkeypatch) -> None:
async def fake_get_briefing(self, *, mode: str): # noqa: ANN001
_ = self
return {
"brief_date": "2026-03-30",
"mode": mode,
"hero": {
"title": "老板晨报",
"decision_count": 2,
"anomaly_count": 4,
},
"sections": [
{
"key": "decisions",
"title": "今日需拍板",
"summary": "有 2 项需要拍板",
"metrics": [{"label": "需拍板", "value": 2, "tone": "warning"}],
"items": [],
"drilldown_tab": "signals",
}
],
}
monkeypatch.setattr(ExecutiveBriefingService, "get_briefing", fake_get_briefing)
app = _build_app(db_session=db_session)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
snapshot_response = await client.get("/ai-steward/executive-briefing", params={"mode": "snapshot"})
live_response = await client.get("/ai-steward/executive-briefing", params={"mode": "live"})
assert snapshot_response.status_code == 200
snapshot_payload = snapshot_response.json()
assert snapshot_payload["code"] == 0
assert snapshot_payload["message"] == "success"
assert snapshot_payload["data"]["mode"] == "snapshot"
assert snapshot_payload["data"]["hero"]["decision_count"] == 2
assert live_response.status_code == 200
live_payload = live_response.json()
assert live_payload["code"] == 0
assert live_payload["data"]["mode"] == "live"
assert live_payload["data"]["sections"][0]["key"] == "decisions"
@pytest.mark.asyncio
async def test_executive_briefing_config_round_trip_is_enveloped(db_session) -> None:
app = _build_app(db_session=db_session)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
default_response = await client.get("/ai-steward/executive-briefing/config")
update_response = await client.put(
"/ai-steward/executive-briefing/config",
json={
"enabled": True,
"send_time": "09:15",
"send_cycle": "daily",
"delivery_target_type": "management_group",
"delivery_target_ref": "oc_management_group",
"timezone": "Asia/Shanghai",
},
)
refreshed_response = await client.get("/ai-steward/executive-briefing/config")
assert default_response.status_code == 200
default_payload = default_response.json()
assert default_payload["code"] == 0
assert default_payload["data"]["send_time"] == "08:30"
assert default_payload["data"]["send_cycle"] == "workdays"
assert update_response.status_code == 200
update_payload = update_response.json()
assert update_payload["code"] == 0
assert update_payload["data"]["send_time"] == "09:15"
assert update_payload["data"]["delivery_target_ref"] == "oc_management_group"
assert update_payload["data"]["updated_by"] == 9001
refreshed_payload = refreshed_response.json()
assert refreshed_payload["code"] == 0
assert refreshed_payload["data"]["send_time"] == "09:15"
assert refreshed_payload["data"]["send_cycle"] == "daily"
@pytest.mark.asyncio
async def test_executive_briefing_config_rejects_non_manager_users_with_envelope(db_session) -> None:
app = _build_app(db_session=db_session, role="BE")
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.put(
"/ai-steward/executive-briefing/config",
json={
"enabled": True,
"send_time": "09:15",
"send_cycle": "daily",
"delivery_target_type": "management_group",
"delivery_target_ref": "oc_management_group",
"timezone": "Asia/Shanghai",
},
)
assert response.status_code == 403
payload = response.json()
assert payload["code"] == 403
assert payload["data"] is None
@pytest.mark.asyncio
async def test_executive_briefing_service_persists_config_in_integration_row(db_session) -> None:
service = ExecutiveBriefingService(db_session, tenant_id=1)
updated = await service.update_config(
payload={
"enabled": True,
"send_time": "09:45",
"send_cycle": "daily",
"delivery_target_type": "management_group",
"delivery_target_ref": "oc_management_group_2",
"timezone": "Asia/Shanghai",
},
operator_user_id=777,
)
await db_session.commit()
assert updated["send_time"] == "09:45"
assert updated["updated_by"] == 777
row = (
await db_session.execute(
Integration.__table__.select().where(Integration.tenant_id == 1, Integration.type == "executive_briefing")
)
).mappings().one()
assert row["config"]["send_time"] == "09:45"
assert row["config"]["delivery_target_ref"] == "oc_management_group_2"
@pytest.mark.asyncio
async def test_executive_briefing_service_formats_management_summary_text(db_session) -> None:
service = ExecutiveBriefingService(db_session, tenant_id=1)
text = service.render_management_group_message(
{
"brief_date": "2026-03-30",
"hero": {
"title": "老板晨报",
"decision_count": 2,
"anomaly_count": 4,
"baseline_generated_at": datetime(2026, 3, 30, 8, 30, 0).isoformat(),
},
"sections": [
{
"title": "今日需拍板",
"summary": "有 2 项需要拍板",
"items": [
{"title": "P0 信号无人接", "detail": "项目 A"},
{"title": "持续过载成员", "detail": "张三 128%"},
],
}
],
}
)
assert "老板晨报" in text
assert "今日需拍板" in text
assert "P0 信号无人接" in text