初始化提交

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,217 @@
"""调制因子派生测试(辰正 §5.2 / §7.2 / §7.3)。"""
from datetime import UTC, datetime
import pytest
from app.modules.time_sense.duration import OfflineDuration
from app.modules.time_sense.lunar import Festival
from app.modules.time_sense.modulation import (
ModulationInput,
compute_modulation,
)
from app.modules.time_sense.snapshot import (
LocationContext,
SnapshotInput,
TemporalSnapshot,
compute_snapshot,
)
def _festival_snapshot(festival_name: str) -> TemporalSnapshot:
"""构造一个命中指定节日、白天的快照(用真实 compute_snapshot)。"""
# 2026-02-17 = 春节正月初一;其它节日按需选日期
festival_dates = {
"春节": "2026-02-17",
"中秋节": "2026-09-25", # 中秋八十五
"国庆节": "2026-10-01",
}
iso = festival_dates.get(festival_name, "2026-07-04")
y, m, d = (int(x) for x in iso.split("-"))
inp = SnapshotInput(
now=datetime(y, m, d, 4, 0, tzinfo=UTC), # UTC 04:00 = 上海 12:00(上午/中午)
timezone="Asia/Shanghai",
location=LocationContext(timezone="Asia/Shanghai", city="上海"),
anniversaries=[],
acquaintance_anchor=datetime(y, m, d, tzinfo=UTC),
last_active_at=datetime(y, m, d, 3, 0, tzinfo=UTC),
)
return compute_snapshot(inp)
def _snapshot(**overrides) -> TemporalSnapshot:
"""构造一个无节日的基线快照(上海上午、周末)。"""
inp = SnapshotInput(
now=datetime(2026, 7, 4, 4, 0, tzinfo=UTC), # 上海 12:00
timezone="Asia/Shanghai",
location=LocationContext(timezone="Asia/Shanghai", city="上海"),
anniversaries=[],
acquaintance_anchor=datetime(2025, 9, 19, tzinfo=UTC),
last_active_at=datetime(2026, 7, 4, 3, 0, tzinfo=UTC),
)
snap = compute_snapshot(inp)
# 允许测试覆盖关键字段
mutated = snap.model_dump()
mutated.update(overrides)
return TemporalSnapshot(**mutated)
def test_festival_joy_no_festival_low():
factors = compute_modulation(_snapshot(), ModulationInput())
assert factors.festival_joy == pytest.approx(0.1, abs=0.01)
def test_festival_joy_spring_festival_high():
snap = _festival_snapshot("春节")
factors = compute_modulation(snap, ModulationInput())
assert factors.festival_joy == pytest.approx(0.9, abs=0.01)
def test_festival_joy_multi_takes_max():
# 多节日同时命中:取最大权重(Finding 3:覆盖 _festival_joy 的 max 聚合路径)
snap = _snapshot(
festivals=[
Festival(name="春节", calendar="lunar"),
Festival(name="元旦", calendar="solar"),
]
)
factors = compute_modulation(snap, ModulationInput())
assert factors.festival_joy == pytest.approx(0.9, abs=0.01)
def test_late_night_fragility_deep_night_with_turns():
# 凌晨 + 连续轮数 → 脆弱度上升
snap = _snapshot(day_period="凌晨", local_hour=2)
f0 = compute_modulation(snap, ModulationInput(conversation_turns_today=0))
f3 = compute_modulation(snap, ModulationInput(conversation_turns_today=3))
assert f0.late_night_fragility == pytest.approx(0.7, abs=0.01)
assert f3.late_night_fragility > f0.late_night_fragility
assert f3.late_night_fragility <= 1.0
def test_late_night_fragility_daytime_low():
factors = compute_modulation(_snapshot(), ModulationInput()) # 上午
assert factors.late_night_fragility == pytest.approx(0.1, abs=0.01)
def test_reunion_intensity_passes_through():
snap = _snapshot(
offline=OfflineDuration(minutes=10000, band="reunion_impact", reunion_intensity=0.85)
)
factors = compute_modulation(snap, ModulationInput())
assert factors.reunion_intensity == pytest.approx(0.85, abs=0.01)
def test_weekend_relax_weekend_with_festival():
snap = _festival_snapshot("国庆节") # 2026-10-01 周四 → 不是周末,手工覆盖
snap = snap.model_copy(update={"is_weekend": True, "festivals": snap.festivals})
factors = compute_modulation(snap, ModulationInput())
assert factors.weekend_relax == pytest.approx(0.9, abs=0.01)
def test_weekend_relax_workday():
snap = _snapshot(is_weekend=False)
factors = compute_modulation(snap, ModulationInput())
assert factors.weekend_relax == pytest.approx(0.2, abs=0.01)
def test_season_resonance_in_range():
factors = compute_modulation(_snapshot(), ModulationInput()) # 夏
assert 0.0 <= factors.season_resonance <= 1.0
assert factors.season_resonance == pytest.approx(0.6, abs=0.01)
def test_same_period_resonance_morning_low():
factors = compute_modulation(_snapshot(), ModulationInput()) # 上午
assert factors.same_period_resonance == pytest.approx(0.2, abs=0.01)
@pytest.mark.parametrize(
"signals, snapshot_overrides, expected_kind",
[
# 深夜 + 时差 ≥ 3h → jetlag
(
{"user_birthday_today": False},
{"day_period": "凌晨", "local_hour": 2, "home_tz_diff_minutes": 200},
"jetlag",
),
# 用户生日 → birthday(优先级高)
({"user_birthday_today": True}, {}, "birthday"),
# 清晨 4-5 点首条消息 → early_morning
(
{"is_first_message_today": True},
{"day_period": "清晨", "local_hour": 5},
"early_morning",
),
# 深夜工作话题 → overwork
({"content_work_related": True}, {"day_period": "深夜", "local_hour": 23}, "overwork"),
# 节日 + 孤独 → festival_lonely
(
{"content_lonely": True},
{"festivals": [Festival(name="春节", calendar="lunar")]},
"festival_lonely",
),
# 凌晨 + 连续 ≥ 3 轮 → late_night
({"conversation_turns_today": 3}, {"day_period": "凌晨", "local_hour": 2}, "late_night"),
# 深夜 + 连续 ≥ 3 轮 → late_night(Finding 1 回归防护:深夜也触发)
({"conversation_turns_today": 3}, {"day_period": "深夜", "local_hour": 23}, "late_night"),
],
)
def test_care_kind_triggers(signals, snapshot_overrides, expected_kind):
snap = _snapshot(**snapshot_overrides)
factors = compute_modulation(snap, ModulationInput(**signals))
assert factors.care_kind == expected_kind
def test_care_kind_none_when_no_trigger():
factors = compute_modulation(_snapshot(), ModulationInput()) # 白天、无信号
assert factors.care_kind is None
def test_care_suggest_high_when_care_kind_hit():
snap = _snapshot(day_period="凌晨", local_hour=2, home_tz_diff_minutes=200)
factors = compute_modulation(snap, ModulationInput())
assert factors.care_suggest >= 0.7
def test_care_suggest_low_when_no_care():
factors = compute_modulation(_snapshot(), ModulationInput()) # 上午
assert factors.care_suggest < 0.2
def test_all_factors_in_unit_range():
snap = _snapshot(day_period="凌晨", local_hour=2)
factors = compute_modulation(snap, ModulationInput(conversation_turns_today=5))
for name in (
"festival_joy",
"late_night_fragility",
"reunion_intensity",
"weekend_relax",
"season_resonance",
"same_period_resonance",
"care_suggest",
):
v = getattr(factors, name)
assert 0.0 <= v <= 1.0, f"{name}={v} 超出 [0,1]"
def test_care_kind_jetlag_threshold_configurable():
"""设计 §7.3:jetlag 阈值经 ModulationInput 可调(守护配置接入,防回归摆设)。"""
snap = _snapshot(day_period="凌晨", local_hour=2, home_tz_diff_minutes=200)
# 默认阈值 180:200 >= 180 → 触发 jetlag
assert compute_modulation(snap, ModulationInput()).care_kind == "jetlag"
# 抬高阈值到 300:200 < 300 → 不再 jetlag(其余条件也不命中 → None)
raised = compute_modulation(snap, ModulationInput(jetlag_threshold_minutes=300))
assert raised.care_kind != "jetlag"
def test_care_kind_late_night_turns_configurable():
"""设计 §7.3:late_night 最小轮数经 ModulationInput 可调。"""
snap = _snapshot(day_period="凌晨", local_hour=2)
# 2 轮 < 默认 3 → 不触发
assert compute_modulation(snap, ModulationInput(conversation_turns_today=2)).care_kind is None
# 阈值降到 2:2 >= 2 → 触发 late_night
lowered = compute_modulation(
snap, ModulationInput(conversation_turns_today=2, late_night_min_turns=2)
)
assert lowered.care_kind == "late_night"