113 lines
4.5 KiB
Python
113 lines
4.5 KiB
Python
"""辰正集成测试:重逢流事件链 + milestone 联动(设计 §6.3)。"""
|
||
|
||
from datetime import UTC, datetime
|
||
|
||
import pytest
|
||
|
||
from app.db.session import AsyncSessionLocal
|
||
from app.models.chat import ChatMessage, ChatSession
|
||
from app.models.time_anniversary import TimeAnniversary
|
||
from app.modules.runtime.events import InMemoryEventStore
|
||
from app.modules.time_sense.modulation import ModulationInput
|
||
from app.services.characters import get_active_character_model
|
||
from app.services.time_sense import compute_live_snapshot
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_reunion_flow_emits_reunion_and_sentiment():
|
||
"""离线 > 3 天 → time.reunion + time.sentiment(reunion_memory)。"""
|
||
async with AsyncSessionLocal() as session:
|
||
character = await get_active_character_model(session)
|
||
chat_session = ChatSession(character_id=character.id, status="active")
|
||
session.add(chat_session)
|
||
await session.flush()
|
||
old_msg = ChatMessage(
|
||
character_id=character.id,
|
||
session_id=chat_session.id,
|
||
role="user",
|
||
content="5 天前的消息",
|
||
created_at=datetime(2026, 6, 29, 4, 0, tzinfo=UTC),
|
||
)
|
||
session.add(old_msg)
|
||
await session.flush()
|
||
|
||
store = InMemoryEventStore()
|
||
result = await compute_live_snapshot(
|
||
session,
|
||
store,
|
||
character.id,
|
||
ModulationInput(),
|
||
now=datetime(2026, 7, 4, 4, 0, tzinfo=UTC), # 5 天后
|
||
)
|
||
|
||
assert result.snapshot is not None
|
||
assert result.snapshot.offline.band == "reunion_impact"
|
||
event_types = {e.type for e in result.emitted_events}
|
||
assert "time.reunion" in event_types
|
||
assert "time.sentiment" in event_types # reunion_memory 触发
|
||
sentiment = next(e for e in result.emitted_events if e.type == "time.sentiment")
|
||
assert sentiment.payload["trigger_type"] == "reunion_memory"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_no_event_when_active():
|
||
"""active 档(无历史消息,last_active=now)→ 无 reunion/sentiment 事件。"""
|
||
async with AsyncSessionLocal() as session:
|
||
character = await get_active_character_model(session)
|
||
store = InMemoryEventStore()
|
||
result = await compute_live_snapshot(
|
||
session,
|
||
store,
|
||
character.id,
|
||
ModulationInput(),
|
||
now=datetime(2026, 7, 4, 4, 0, tzinfo=UTC), # 上海 12:00 上午
|
||
)
|
||
assert result.snapshot is not None
|
||
assert result.snapshot.offline.band == "active"
|
||
event_types = {e.type for e in result.emitted_events}
|
||
assert "time.reunion" not in event_types
|
||
assert "time.sentiment" not in event_types
|
||
assert "time.user_care" not in event_types # 白天 + 无 birthday 等信号
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_milestone_at_late_night_emits_sentiment_overlay():
|
||
"""凌晨 + 今日命中纪念日 → active_milestones 非空 + time.sentiment(late_night_overlay)。
|
||
|
||
验证 anniversary 命中 + 凌晨时段联动 sentiment 的集成路径(Task 4 未覆盖)。
|
||
"""
|
||
async with AsyncSessionLocal() as session:
|
||
character = await get_active_character_model(session)
|
||
anni = TimeAnniversary(
|
||
character_id=character.id,
|
||
name="我们的相遇日",
|
||
calendar="solar",
|
||
date="2026-07-05", # 今日(now=18:00 UTC → 上海 02:00 本地日期 07-05)命中
|
||
recurs_yearly=True,
|
||
kind="anniversary",
|
||
subject_type="relationship",
|
||
source="manual",
|
||
)
|
||
session.add(anni)
|
||
await session.flush()
|
||
|
||
store = InMemoryEventStore()
|
||
result = await compute_live_snapshot(
|
||
session,
|
||
store,
|
||
character.id,
|
||
ModulationInput(),
|
||
now=datetime(2026, 7, 4, 18, 0, tzinfo=UTC), # 上海 02:00 凌晨
|
||
)
|
||
|
||
assert result.snapshot is not None
|
||
assert result.snapshot.day_period == "凌晨"
|
||
assert len(result.snapshot.active_milestones) >= 1
|
||
# duration_node(0 不在 nodes)/anniversary_prewarm(next=365>7)/reunion_memory(active 档) 都不触发
|
||
# → late_night_overlay(凌晨 + has_active_milestone)
|
||
sentiment_events = [e for e in result.emitted_events if e.type == "time.sentiment"]
|
||
assert len(sentiment_events) == 1
|
||
assert sentiment_events[0].payload["trigger_type"] == "late_night_overlay"
|
||
# active 档(无 chat 历史)→ 不触发 reunion
|
||
assert "time.reunion" not in {e.type for e in result.emitted_events}
|