PMFlow/backend/tests/test_daily_briefing_service.py

144 lines
4.5 KiB
Python

from __future__ import annotations
import os
import sys
import types
from datetime import datetime, timedelta
import pytest
import pytest_asyncio
from sqlalchemy import JSON
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///:memory:"
from app.models.base import Base
from app.models.cognitive_conflict import CognitiveConflict
from app.models.project import Project
from app.models.project_signal_link import ProjectSignalLink
from app.models.signal import Signal
from app.models.task import Task
from app.models.truth_node import TruthNode
from app.models.user import User
def _use_sqlite_json_types() -> None:
User.__table__.c.assigned_domains.type = JSON()
TruthNode.__table__.c.content.type = JSON()
Task.__table__.c.ai_recommended_assignees.type = JSON()
Task.__table__.c.ai_breakdown_payload.type = JSON()
Task.__table__.c.manual_overrides.type = JSON()
Task.__table__.c.required_skill_tag_ids.type = JSON()
CognitiveConflict.__table__.c.involved_user_ids.type = JSON()
@pytest_asyncio.fixture
async def briefing_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=[
User.__table__,
Project.__table__,
TruthNode.__table__,
Task.__table__,
Signal.__table__,
ProjectSignalLink.__table__,
CognitiveConflict.__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_generate_personalized_briefing_uses_naive_time_window(briefing_session, monkeypatch) -> None:
fixed_now = datetime(2026, 3, 25, 14, 43, 24)
stub_database_module = types.ModuleType("app.core.database")
stub_database_module.AsyncSessionLocal = None
monkeypatch.setitem(sys.modules, "app.core.database", stub_database_module)
import app.services.daily_briefing_core.mixin_personal as daily_briefing_personal_module
from app.services.daily_briefing_core import DailyBriefingService
monkeypatch.setattr(daily_briefing_personal_module, "now_cn_naive", lambda: fixed_now)
async def fake_generate_ai_summary(self, user, data, tenant_id): # noqa: ANN001
_ = self, user, data, tenant_id
return "今日简报"
monkeypatch.setattr(DailyBriefingService, "_generate_ai_summary", fake_generate_ai_summary)
user = User(
tenant_id=1,
username="tester",
email="tester@example.com",
password_hash="hash",
name="测试用户",
role="BE",
status="active",
is_active=True,
)
project = Project(
tenant_id=1,
name="demo-project",
title="Demo Project",
status="active",
)
briefing_session.add_all([user, project])
await briefing_session.flush()
truth_node = TruthNode(
tenant_id=1,
node_type="task",
parent_id=None,
project_id=project.id,
signal_id=None,
created_by=user.id,
title="修复简报时间窗",
content={},
version=1,
status="active",
)
briefing_session.add(truth_node)
await briefing_session.flush()
task = Task(
tenant_id=1,
truth_node_id=truth_node.id,
project_id=project.id,
assignee_id=user.id,
ai_recommended_assignees={},
ai_breakdown_payload=None,
manual_overrides=None,
title="修复简报时间窗",
description="",
required_skill_tag_ids=[],
status="in_progress",
priority="HIGH",
estimated_hours=2.0,
actual_hours=None,
quality_score=None,
due_date=None,
completed_at=None,
created_at=fixed_now - timedelta(hours=4),
updated_at=fixed_now - timedelta(hours=2),
)
briefing_session.add(task)
await briefing_session.commit()
briefing = await DailyBriefingService(briefing_session).generate_personalized_briefing(
user_id=user.id,
tenant_id=1,
)
assert briefing["summary"] == "今日简报"
assert len(briefing["your_tasks"]) == 1
assert briefing["your_tasks"][0]["title"] == "修复简报时间窗"