初始化提交
This commit is contained in:
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"
|
||||
Reference in new issue
Block a user