"""sessions / staff-chat-stats 的汇总表读路径判别测试(2026-09-03)。 两个端点在 msg_daily_stats 有行时必须改读汇总表(毫秒级),空表时回退旧 实时聚合(部署回填前的守卫)。判别手法:**回填汇总后把 message 表删光**—— 旧实现会返回空,只有真正读汇总表的实现还能吐出完整数据。 另配对拍测试:同一批种子,「无汇总(旧路径)」与「有汇总(新路径)」的 响应必须逐字段一致。 """ from __future__ import annotations import time import sqlalchemy as sa from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import ChatGroup, Customer, Message from app.storage.summary import recompute_all OWNER = "wx_owner_sum" CH = "wechat" _NOW = int(time.time() * 1000) _MS_DAY = 86_400_000 async def _seed(session: AsyncSession) -> None: session.add_all([ # t1:今天 1 发 1 收,2 天前还有一条 Message(msg_id="s1", channel=CH, owner_account_id=OWNER, talker_id="t1", room_id=None, is_send=True, timestamp=_NOW, type="text", type_raw=1, text="x"), Message(msg_id="s2", channel=CH, owner_account_id=OWNER, talker_id="t1", room_id=None, is_send=False, timestamp=_NOW - 1000, type="text", type_raw=1, text="y"), Message(msg_id="s3", channel=CH, owner_account_id=OWNER, talker_id="t1", room_id=None, is_send=False, timestamp=_NOW - 2 * _MS_DAY, type="text", type_raw=1, text="old"), # t2:只有 5 天前的消息(今日 0) Message(msg_id="s4", channel=CH, owner_account_id=OWNER, talker_id="t2", room_id=None, is_send=True, timestamp=_NOW - 5 * _MS_DAY, type="text", type_raw=1, text="o2"), # 群:今天 2 条 Message(msg_id="s5", channel=CH, owner_account_id=OWNER, talker_id="m1", room_id="room1@chatroom", is_send=True, timestamp=_NOW, type="text", type_raw=1, text="g1"), Message(msg_id="s6", channel=CH, owner_account_id=OWNER, talker_id="m2", room_id="room1@chatroom", is_send=False, timestamp=_NOW - 2000, type="text", type_raw=1, text="g2"), Customer(friend_id="t1", channel=CH, owner_account_id=OWNER, friend_nickname="Alice", friend_remark=None), Customer(friend_id="t2", channel=CH, owner_account_id=OWNER, friend_nickname="Bob", friend_remark=None), ChatGroup(room_id="room1@chatroom", channel=CH, owner_account_id=OWNER, room_name="群一号"), ]) await session.commit() async def _seed_with_stats(session: AsyncSession) -> None: await _seed(session) await recompute_all(session) await session.commit() async def _stats_then_wipe(session: AsyncSession) -> None: await _seed_with_stats(session) await session.execute(sa.delete(Message)) await session.commit() # ── sessions ─────────────────────────────────────────────────────────────── # async def test_sessions_served_from_summary_when_populated(make_client): """回填后删光 message:响应仍完整 → 数据来自汇总表(旧实现此刻返回空)。""" wiped = await make_client(_stats_then_wipe) r = await wiped.get("/api/sessions", params={"owner_account_id": OWNER, "channel": CH}) assert r.status_code == 200 body = r.json() by = {(s["type"], s.get("talker_id") or s.get("room_id")): s for s in body["sessions"]} assert set(by) == {("friend", "t1"), ("friend", "t2"), ("group", "room1@chatroom")} assert by[("friend", "t1")]["today_count"] == 2 assert by[("friend", "t1")]["last_chat_time"] == _NOW assert by[("friend", "t2")]["today_count"] == 0 assert by[("group", "room1@chatroom")]["today_count"] == 2 assert by[("friend", "t1")]["name"] == "Alice" async def test_sessions_parity_and_legacy_guard(make_client): """对拍:无汇总(守卫回退旧路径)响应 == 有汇总(新路径)响应,且逐字段一致。""" legacy = await make_client(_seed) modern = await make_client(_seed_with_stats) params = {"owner_account_id": OWNER, "channel": CH} r1 = await legacy.get("/api/sessions", params=params) r2 = await modern.get("/api/sessions", params=params) assert r1.status_code == r2.status_code == 200 assert r1.json() == r2.json() # ── staff-chat-stats ─────────────────────────────────────────────────────── # async def test_staff_chat_stats_served_from_summary(make_client): """全时段窗口(旧实现实测 6.9s 的场景):回填后删光 message 仍出全量数字。""" client = await make_client(_stats_then_wipe) r = await client.get( "/api/staff-chat-stats", params={"owner_account_id": OWNER, "start": 0, "end": _NOW + _MS_DAY}, ) assert r.status_code == 200 body = r.json() by = {c["talker_id"]: c for c in body["customers"]} assert set(by) == {"t1", "t2"} # 私聊口径,群不计 assert by["t1"]["send_count"] == 1 and by["t1"]["receive_count"] == 2 assert by["t1"]["last_time"] == _NOW assert by["t2"]["send_count"] == 1 and by["t2"]["receive_count"] == 0 async def test_staff_chat_stats_parity(make_client): """宽窗口:旧路径与汇总路径响应一致。""" legacy = await make_client(_seed) modern = await make_client(_seed_with_stats) params = {"owner_account_id": OWNER, "start": 0, "end": _NOW + _MS_DAY} r1 = await legacy.get("/api/staff-chat-stats", params=params) r2 = await modern.get("/api/staff-chat-stats", params=params) assert r1.status_code == r2.status_code == 200 assert r1.json() == r2.json()