fix(analytics): unreplied is_send守卫防漂移假行+真预算注释+501候选分支测试
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -138,9 +138,6 @@ def test_next_tier_bad_tier_raises_value_error():
|
||||
assert metrics._next_tier(90) == 10_000 # 90 档=≥90 的开区间上限
|
||||
|
||||
|
||||
_VALID_TYPES = ("text", "image", "voice", "video", "file", "link")
|
||||
|
||||
|
||||
async def _seed_unreplied(session):
|
||||
now = _NOW
|
||||
h3 = 3 * 3600 * 1000
|
||||
@@ -283,3 +280,63 @@ async def test_unreplied_endpoint_422(make_client):
|
||||
assert r.status_code == 422
|
||||
r2 = await client.get("/api/analytics/unreplied", params={"channel": "sms"})
|
||||
assert r2.status_code == 422
|
||||
|
||||
|
||||
async def test_unreplied_stats_message_drift_staff_tail_excluded(
|
||||
make_client, monkeypatch
|
||||
):
|
||||
"""stats/message 漂移守卫:统计行客户尾向超时,但 message 真实尾部是员工回复 → 不在清单。
|
||||
|
||||
写钩子吞重算失败、夜间重建兜底前 stats 可能滞后(一等状态);决赛圈以
|
||||
message 实尾为准——is_send=True=员工尾部=定义上非待回复(is_send=None
|
||||
保留=与统计同向保守)。
|
||||
"""
|
||||
monkeypatch.setattr(metrics, "_now_ms", lambda: _NOW)
|
||||
async def seed(session):
|
||||
now = _NOW
|
||||
h3 = 3 * 3600 * 1000
|
||||
session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1",
|
||||
peer_kind="friend", peer_id="t9", day=20260904,
|
||||
send_cnt=5, recv_cnt=3, last_ts=now - h3,
|
||||
last_is_send=False))
|
||||
# 真实尾部:员工 1h 前的回复(stats 行漂移,仍停在客户尾向 3h 前)
|
||||
session.add(Message(msg_id="t9m", channel="wechat", owner_account_id="wx1",
|
||||
talker_id="t9", timestamp=now - 3600 * 1000,
|
||||
is_send=True, type="text", text="已经回了哈",
|
||||
fetched_at=dt.datetime(2026, 9, 4, 12, 0)))
|
||||
out = await metrics.unreplied(session, channel="wechat", hours=2,
|
||||
page=1, page_size=20)
|
||||
assert all(r["peer_id"] != "t9" for r in out["items"])
|
||||
|
||||
await make_client(seed)
|
||||
|
||||
|
||||
async def test_unreplied_large_candidate_branch(make_client, monkeypatch):
|
||||
"""候选 > _FINALIZE_ALL_MAX:按候选分页,total=候选数;决赛圈过滤后本页可少几行。
|
||||
|
||||
501 个候选只给 bulk000 一条 message 行 → 第 1 页 items 仅 1 行(而非
|
||||
page_size 100),锁定「total=候选数、items 可能少几行」的文档口径;
|
||||
同时证明大候选分支的决赛圈点查真实执行(有效行保留)。
|
||||
"""
|
||||
monkeypatch.setattr(metrics, "_now_ms", lambda: _NOW)
|
||||
async def seed(session):
|
||||
now = _NOW
|
||||
h3 = 3 * 3600 * 1000
|
||||
for i in range(metrics._FINALIZE_ALL_MAX + 1):
|
||||
session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1",
|
||||
peer_kind="friend", peer_id=f"bulk{i:03d}",
|
||||
day=20260904, send_cnt=5, recv_cnt=3,
|
||||
last_ts=now - h3, last_is_send=False))
|
||||
# ts 全等 → 按 peer asc 决胜,bulk000 排第 1 页首行
|
||||
session.add(Message(msg_id="b0m", channel="wechat", owner_account_id="wx1",
|
||||
talker_id="bulk000", timestamp=now - h3,
|
||||
is_send=False, type="text", text="在吗",
|
||||
fetched_at=dt.datetime(2026, 9, 4, 12, 0)))
|
||||
await session.commit()
|
||||
out = await metrics.unreplied(session, channel="wechat", hours=2,
|
||||
page=1, page_size=100)
|
||||
assert out["total"] == metrics._FINALIZE_ALL_MAX + 1 # total=候选数
|
||||
ids = [r["peer_id"] for r in out["items"]]
|
||||
assert ids == ["bulk000"] # 无 message 行的候选决赛圈全滤=口径内少几行
|
||||
|
||||
await make_client(seed)
|
||||
|
||||
Reference in New Issue
Block a user