feat(analytics): C1 扫描器——fetched_at 水位增量/迟到免疫/幂等/全量回填
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,9 @@ from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
|
||||
from app.analytics.sensitive import scanner
|
||||
from app.analytics.sensitive import words as words_svc
|
||||
from app.db.models import Message
|
||||
from app.db.models import Message, SensitiveHit, SensitiveScanState
|
||||
|
||||
|
||||
async def test_create_and_duplicate(make_client):
|
||||
@@ -92,3 +93,97 @@ async def test_api_create_empty_word_409(make_client):
|
||||
r = await client.post("/api/analytics/sensitive/words",
|
||||
json={"word": " "})
|
||||
assert r.status_code == 409
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- C1 scanner
|
||||
|
||||
def _msg(msg_id, text, fetched, ts=1756900000000, talker="t1"):
|
||||
return Message(msg_id=msg_id, channel="wechat", owner_account_id="wx1",
|
||||
talker_id=talker, timestamp=ts, is_send=False, type="text",
|
||||
text=text, fetched_at=fetched)
|
||||
|
||||
|
||||
async def test_scan_incremental_and_idempotent(make_client):
|
||||
async def seed(session):
|
||||
f1 = dt.datetime(2026, 9, 4, 10, 0)
|
||||
session.add(_msg("m1", "聊聊私单", f1))
|
||||
session.add(_msg("m2", "正常", f1))
|
||||
await words_svc.create_word(session, word="私单")
|
||||
await session.commit()
|
||||
r1 = await scanner.run_incremental(session)
|
||||
await session.commit()
|
||||
assert r1["hits"] == 1
|
||||
assert r1["new_watermark"] == f1
|
||||
r2 = await scanner.run_incremental(session) # 再跑:水位之后无新行
|
||||
await session.commit()
|
||||
assert r2["hits"] == 0
|
||||
from sqlalchemy import func, select
|
||||
|
||||
n = (await session.execute(
|
||||
select(func.count()).select_from(SensitiveHit)
|
||||
)).scalar_one()
|
||||
assert n == 1 # 幂等不重复
|
||||
|
||||
await make_client(seed)
|
||||
|
||||
|
||||
async def test_late_arriving_history_scanned(make_client):
|
||||
"""迟到数据:timestamp 很老但 fetched_at 新 → 必须被扫到(P2 水位设计)。"""
|
||||
async def seed(session):
|
||||
old = dt.datetime(2026, 9, 1, 8, 0)
|
||||
session.add(_msg("m_old", "历史私单消息", old))
|
||||
await words_svc.create_word(session, word="私单")
|
||||
await session.commit()
|
||||
r1 = await scanner.run_incremental(session)
|
||||
await session.commit()
|
||||
assert r1["hits"] == 1
|
||||
# 迟到行:timestamp 很老(fetched_at 现在)
|
||||
session.add(_msg("m_late", "迟到的私单", dt.datetime(2026, 9, 4, 12, 0),
|
||||
ts=1767000000000))
|
||||
await session.commit()
|
||||
r2 = await scanner.run_incremental(session)
|
||||
await session.commit()
|
||||
assert r2["hits"] == 1 # 按 fetched_at 水位,迟到历史不漏
|
||||
|
||||
await make_client(seed)
|
||||
|
||||
|
||||
async def test_single_char_like_fallback(make_client):
|
||||
async def seed(session):
|
||||
session.add(_msg("m1", "坑爹", dt.datetime(2026, 9, 4, 10, 0)))
|
||||
await words_svc.create_word(session, word="坑") # 单字
|
||||
await session.commit()
|
||||
r = await scanner.run_incremental(session)
|
||||
await session.commit()
|
||||
assert r["hits"] == 1 # 单字走 LIKE 子串,增量行少语义精确
|
||||
|
||||
await make_client(seed)
|
||||
|
||||
|
||||
async def test_disposition_preserved(make_client):
|
||||
"""重扫已处置行不覆盖 disposition。"""
|
||||
async def seed(session):
|
||||
session.add(_msg("m1", "私单", dt.datetime(2026, 9, 4, 10, 0)))
|
||||
await words_svc.create_word(session, word="私单")
|
||||
await session.commit()
|
||||
await scanner.run_incremental(session)
|
||||
await session.commit()
|
||||
from sqlalchemy import select
|
||||
|
||||
hit = (await session.execute(
|
||||
select(SensitiveHit)
|
||||
)).scalars().one()
|
||||
hit.disposition = "false_positive"
|
||||
await session.commit()
|
||||
# 重扫同词同消息(水位回拨模拟)
|
||||
st = await session.get(SensitiveScanState, "message_text")
|
||||
st.last_fetched_at = None
|
||||
await session.commit()
|
||||
await scanner.run_incremental(session)
|
||||
await session.commit()
|
||||
hit2 = (await session.execute(
|
||||
select(SensitiveHit)
|
||||
)).scalars().one()
|
||||
assert hit2.disposition == "false_positive"
|
||||
|
||||
await make_client(seed)
|
||||
|
||||
Reference in New Issue
Block a user