"""/api/analytics/health + /overview:E0-lite 与 A1 冒烟。""" from __future__ import annotations import datetime as dt from app.analytics import metrics from app.db.models import Customer, MsgDailyStats, StatDailyStaff, WechatAccount # naive:updated_at 列是无时区 DateTime;fromisoformat 构造不触发 DTZ 检查 _SEEN = "2026-09-04T12:00:00" # overview 的 today_key 是真实今天——固定 day 键的 seed 必须 monkeypatch _TODAY = 20260904 async def test_health_empty(make_client): async def seed(session): ... client = await make_client(seed) r = await client.get("/api/analytics/health") assert r.status_code == 200 body = r.json() assert body["data_as_of"] is None assert len(body["entities"]) == 6 assert all(e["last_seen"] is None for e in body["entities"]) async def test_health_with_stats(make_client): async def seed(session): session.add( MsgDailyStats( channel="wechat", owner_account_id="wx1", peer_kind="friend", peer_id="t1", day=20260904, send_cnt=1, recv_cnt=2, last_ts=1756900000000, last_is_send=False, ) ) session.add( Customer( friend_id="f1", channel="wechat", updated_at=dt.datetime.fromisoformat(_SEEN), ) ) await session.commit() client = await make_client(seed) r = await client.get("/api/analytics/health") assert r.status_code == 200 assert r.json()["data_as_of"] == 1756900000000 entities = {e["table"]: e["last_seen"] for e in r.json()["entities"]} # 前端契约:last_seen 必须是 ISO 字符串(fromisoformat 可解析) assert dt.datetime.fromisoformat(entities["customer"]) == dt.datetime.fromisoformat( _SEEN ) async def test_overview_endpoint(make_client, monkeypatch): monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1", peer_kind="friend", peer_id="t1", day=_TODAY, send_cnt=4, recv_cnt=1, last_ts=1788500000000, last_is_send=True)) await session.commit() client = await make_client(seed) r = await client.get( "/api/analytics/overview", params={"channel": "wechat", "scope": "today"} ) assert r.status_code == 200 body = r.json() assert body["cards"]["msg_total"] == 5 assert isinstance(body["trend"], list) and body["trend"] # scope 非法 → 422 r2 = await client.get( "/api/analytics/overview", params={"channel": "wechat", "scope": "year"} ) assert r2.status_code == 422 # channel 非法 → 422(同一 Literal 校验路径) r3 = await client.get( "/api/analytics/overview", params={"channel": "sms", "scope": "today"} ) assert r3.status_code == 422 async def test_overview_unmapped_accounts(make_client, monkeypatch): """控制器追加:cards 带 unmapped_accounts 计数(spec §8「未映射账号按账号计并标注」的标注通道)。""" monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): session.add(WechatAccount(wx_id="wx1", user_id="u1")) # wx2 无账号表记录 → 未映射 session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1", peer_kind="friend", peer_id="t1", day=_TODAY, send_cnt=2, recv_cnt=0, last_ts=1788500000000, last_is_send=True)) session.add(MsgDailyStats(channel="wechat", owner_account_id="wx2", peer_kind="friend", peer_id="t2", day=_TODAY, send_cnt=1, recv_cnt=0, last_ts=1788501000000, last_is_send=True)) await session.commit() client = await make_client(seed) r = await client.get( "/api/analytics/overview", params={"channel": "wechat", "scope": "today"} ) body = r.json() assert body["cards"]["active_staff"] == 2 # u1 + wx2(按账号计) assert body["cards"]["unmapped_accounts"] == 1 # wx2 标注 async def test_overview_unmapped_rollup(make_client, monkeypatch): """rollup 路径未映射口径与回退路径一致:当窗 user_id 不在映射值集 → 未映射。 recompute 以 amap.get(owner, owner) 落表,映射不上的 owner 以原值进 stat_daily_staff(user_id=wx2);account_map.values() 只含在册员工 user_id, wx2 落在值集之外 → 被识别并标注。 """ monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): session.add(WechatAccount(wx_id="wx1", user_id="u1")) session.add(StatDailyStaff(day=_TODAY, channel="wechat", user_id="u1", msg_sent=2, msg_recv=0, active_friends=1, active_rooms=0)) session.add(StatDailyStaff(day=_TODAY, channel="wechat", user_id="wx2", msg_sent=1, msg_recv=0, active_friends=1, active_rooms=0)) session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1", peer_kind="friend", peer_id="t1", day=_TODAY, send_cnt=2, recv_cnt=0, last_ts=1788500000000, last_is_send=True)) await session.commit() client = await make_client(seed) r = await client.get( "/api/analytics/overview", params={"channel": "wechat", "scope": "today"} ) body = r.json() assert body["cards"]["active_staff"] == 2 assert body["cards"]["unmapped_accounts"] == 1