初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

@@ -0,0 +1,112 @@
"""辰正集成测试:重逢流事件链 + 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}