229 lines
8.0 KiB
Python
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
|