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