Files
mindfulness/server/tests/test_observability.py
2026-02-02 16:47:37 +08:00

130 lines
4.0 KiB
Python
Raw 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.
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from app.features.personalized_reco.observability.builder import RecoMetaBuilder
from app.features.personalized_reco.observability.utils import compute_empty_reason, compute_missing_fields
from app.features.user_profile_scoring.types import ProfileAnswered, UserProfileV1_2, UserStageOneHot
def _u(*, need: dict | None = None, context: dict | None = None, emotion_score=None, conf_u: float = 0.9) -> UserProfileV1_2:
now = datetime.now(tz=timezone.utc)
return UserProfileV1_2(
profile_generated_at=now,
profile_confidence=conf_u,
profile_answered=ProfileAnswered(stage=True, emotion=True, context=True, need=True),
stage=UserStageOneHot(unknown=1),
emotion_score=emotion_score,
context=context or {},
need=need or {},
)
def test_compute_missing_fields() -> None:
u1 = _u(need={}, context={}, emotion_score=None)
m1 = compute_missing_fields(u1)
assert m1.need is True
assert m1.context is True
assert m1.emotion is True
u2 = _u(need={"x": 1}, context={"y": 1}, emotion_score=0.6)
m2 = compute_missing_fields(u2)
assert m2.need is False
assert m2.context is False
assert m2.emotion is False
def test_compute_empty_reason_branches() -> None:
assert (
compute_empty_reason(
served_k=1,
candidate_pool_size_raw=0,
candidate_pool_size_after_hard_filter=0,
candidate_pool_size_after_freqcap=0,
)
is None
)
assert (
compute_empty_reason(
served_k=0,
candidate_pool_size_raw=0,
candidate_pool_size_after_hard_filter=0,
candidate_pool_size_after_freqcap=0,
)
== "pool_empty"
)
assert (
compute_empty_reason(
served_k=0,
candidate_pool_size_raw=10,
candidate_pool_size_after_hard_filter=0,
candidate_pool_size_after_freqcap=0,
)
== "hard_filter_all"
)
assert (
compute_empty_reason(
served_k=0,
candidate_pool_size_raw=10,
candidate_pool_size_after_hard_filter=5,
candidate_pool_size_after_freqcap=0,
)
== "freqcap_all"
)
assert (
compute_empty_reason(
served_k=0,
candidate_pool_size_raw=10,
candidate_pool_size_after_hard_filter=5,
candidate_pool_size_after_freqcap=3,
)
== "unknown"
)
def test_builder_outputs_stable_fields_and_monotonic_counts() -> None:
u = _u(need={"emotional_support": 1}, context={}, emotion_score=None, conf_u=0.2)
# 故意设置“非单调”的输入,验证 builder 的防御修正
meta = (
RecoMetaBuilder(scene="feed", user_profile=u, k=30)
.set_candidate_pool_size_raw(10)
.set_after_hard_filter(12) # 非法:大于 raw
.set_after_dedup(20) # 非法:大于 after_hard
.set_after_freqcap(15) # 非法:大于 after_dedup修正后会与 after_dedup 对齐)
.set_served_k(99) # 非法:大于 after_freqcap
.set_fallback_level_final(1, reason="freqcap_all")
.build()
)
d = meta.model_dump()
for k in [
"scene",
"candidate_pool_size_raw",
"candidate_pool_size_after_hard_filter",
"candidate_pool_size_after_dedup",
"candidate_pool_size_after_freqcap",
"fallback_level_final",
"served_k",
"empty_reason",
"conf_U",
"missing_fields",
]:
assert k in d
assert meta.candidate_pool_size_raw == 10
assert meta.candidate_pool_size_after_hard_filter == 10
assert meta.candidate_pool_size_after_dedup == 10
assert meta.candidate_pool_size_after_freqcap == 10
assert meta.served_k == 10
assert meta.conf_U == pytest.approx(0.2)
assert meta.missing_fields.context is True
assert meta.missing_fields.emotion is True