Files
hunjing/backend/tests/services/test_time_sense_service.py
2026-08-21 16:00:13 +08:00

138 lines
5.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""辰正 service 测试(流 A 实时快照 + 流 C 事件触发)。"""
from datetime import UTC, datetime, timedelta
import pytest
from app.db.session import AsyncSessionLocal
from app.models.chat import ChatMessage, ChatSession
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_compute_live_snapshot_returns_snapshot_and_factors():
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.factors is not None
assert result.context_text # 非空
assert "周" in result.context_text
@pytest.mark.asyncio
async def test_compute_live_snapshot_degrades_on_failure():
"""计算失败时降级返回最小快照(不抛异常)。"""
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),
timezone="Invalid/Zone",
)
# 降级:snapshot 仍返回(compute_snapshot 内部降级为 UTC),不抛
assert result.snapshot is not None
@pytest.mark.asyncio
async def test_emits_user_care_when_birthday():
"""user_birthday_today → care_kind=birthday → 发 time.user_care(不依赖 snapshot 时段/时区)。"""
async with AsyncSessionLocal() as session:
character = await get_active_character_model(session)
store = InMemoryEventStore()
result = await compute_live_snapshot(
session,
store,
character.id,
ModulationInput(user_birthday_today=True),
now=datetime(2026, 7, 4, 4, 0, tzinfo=UTC), # 上海 12:00
)
assert result.snapshot is not None
care_events = [e for e in result.emitted_events if e.type == "time.user_care"]
assert len(care_events) == 1
assert care_events[0].payload["care_kind"] == "birthday"
# birthday 不应触发 reunion(last_active≈now),也不命中 sentiment 节点
assert all(e.type != "time.reunion" for e in result.emitted_events)
@pytest.mark.asyncio
async def test_emits_reunion_when_offline_exceeds_threshold():
"""seed 5 天前消息 → offline.band=reunion_impact → 发 time.reunion + time.sentiment。"""
now = datetime(2026, 7, 4, 4, 0, tzinfo=UTC)
async with AsyncSessionLocal() as session:
character = await get_active_character_model(session)
# 先建会话(FK 约束),再 seed 一条 5 天前的消息
chat_session = ChatSession(character_id=character.id, status="active")
session.add(chat_session)
await session.flush()
session.add(
ChatMessage(
character_id=character.id,
session_id=chat_session.id,
role="user",
content="旧消息",
created_at=now - timedelta(days=5),
)
)
await session.flush()
store = InMemoryEventStore()
result = await compute_live_snapshot(
session,
store,
character.id,
ModulationInput(),
now=now,
)
assert result.snapshot is not None
assert result.snapshot.offline.band == "reunion_impact"
# 5 天 = 7200 min > 4320 min 阈值 → 重逢
reunion_events = [e for e in result.emitted_events if e.type == "time.reunion"]
assert len(reunion_events) == 1
payload = reunion_events[0].payload
assert payload["offline_band"] == "reunion_impact"
assert "reunion_intensity" in payload
assert payload["offline_minutes"] >= 4320
assert reunion_events[0].sensitivity == "medium"
# is_reunion=True + has_reunion_memory=is_reunion → sentiment=reunion_memory
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"] == "reunion_memory"
@pytest.mark.asyncio
async def test_compute_live_snapshot_attaches_run_id_to_events():
"""对话入口传 run_id → 触发的 time.* 事件全部带该 run_id(落当前 run,SSE 可见)。"""
async with AsyncSessionLocal() as session:
character = await get_active_character_model(session)
store = InMemoryEventStore()
# 凌晨场景触发 late_night care / sentiment
result = await compute_live_snapshot(
session,
store,
character.id,
ModulationInput(conversation_turns_today=3),
now=datetime(2026, 7, 4, 18, 0, tzinfo=UTC), # 上海 02:00 凌晨
timezone="Asia/Shanghai",
run_id="run_test_plan4",
)
assert result.emitted_events, "凌晨 + 3 轮应触发至少一个 time.* 事件"
for ev in result.emitted_events:
assert ev.run_id == "run_test_plan4"