test(analytics): health 契约加深——ISO 序列化断言/空库6实体/诊断端点500有意注释
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ msg_daily_stats 当日有行 = 4h 消息链路活着。
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -22,15 +23,23 @@ from app.db.models import (
|
|||||||
|
|
||||||
|
|
||||||
async def data_as_of(session: AsyncSession) -> int | None:
|
async def data_as_of(session: AsyncSession) -> int | None:
|
||||||
"""数据截至(ms-epoch)= MAX(msg_daily_stats.last_ts)。"""
|
"""数据截至(ms-epoch)= MAX(msg_daily_stats.last_ts)。
|
||||||
|
|
||||||
|
漏迁移导致表缺失时按 500 响亮报错是有意的——本模块是诊断端点,
|
||||||
|
要暴露部署问题而非掩盖(0ade58a 的空表静默守卫是业务端点的容错,
|
||||||
|
别好心搬到这里)。
|
||||||
|
"""
|
||||||
val = (
|
val = (
|
||||||
await session.execute(sa.select(sa.func.max(MsgDailyStats.last_ts)))
|
await session.execute(sa.select(sa.func.max(MsgDailyStats.last_ts)))
|
||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
return int(val) if val is not None else None
|
return int(val) if val is not None else None
|
||||||
|
|
||||||
|
|
||||||
async def entity_freshness(session: AsyncSession) -> list[dict]:
|
async def entity_freshness(session: AsyncSession) -> list[dict[str, Any]]:
|
||||||
"""各实体最后更新时间(UPDATED_AT 列 MAX;message 用 fetched_at)。"""
|
"""各实体最后更新时间(UPDATED_AT 列 MAX;message 用 fetched_at)。
|
||||||
|
|
||||||
|
同 data_as_of:表缺失按 500 响亮报错是有意的(诊断端点不容错)。
|
||||||
|
"""
|
||||||
|
|
||||||
async def _max(col) -> dt.datetime | None:
|
async def _max(col) -> dt.datetime | None:
|
||||||
return (await session.execute(sa.select(sa.func.max(col)))).scalar_one_or_none()
|
return (await session.execute(sa.select(sa.func.max(col)))).scalar_one_or_none()
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
"""/api/analytics/health:E0-lite 冒烟。"""
|
"""/api/analytics/health:E0-lite 冒烟。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.db.models import MsgDailyStats
|
import datetime as dt
|
||||||
|
|
||||||
|
from app.db.models import Customer, MsgDailyStats
|
||||||
|
|
||||||
|
# naive:updated_at 列是无时区 DateTime;fromisoformat 构造不触发 DTZ 检查
|
||||||
|
_SEEN = "2026-09-04T12:00:00"
|
||||||
|
|
||||||
|
|
||||||
async def test_health_empty(make_client):
|
async def test_health_empty(make_client):
|
||||||
@@ -13,7 +18,8 @@ async def test_health_empty(make_client):
|
|||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
body = r.json()
|
body = r.json()
|
||||||
assert body["data_as_of"] is None
|
assert body["data_as_of"] is None
|
||||||
assert any(e["table"] == "message" for e in body["entities"])
|
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 test_health_with_stats(make_client):
|
||||||
@@ -31,9 +37,21 @@ async def test_health_with_stats(make_client):
|
|||||||
last_is_send=False,
|
last_is_send=False,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
session.add(
|
||||||
|
Customer(
|
||||||
|
friend_id="f1",
|
||||||
|
channel="wechat",
|
||||||
|
updated_at=dt.datetime.fromisoformat(_SEEN),
|
||||||
|
)
|
||||||
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
client = await make_client(seed)
|
client = await make_client(seed)
|
||||||
r = await client.get("/api/analytics/health")
|
r = await client.get("/api/analytics/health")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.json()["data_as_of"] == 1756900000000
|
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
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user