fix(report): 空的任务集合被当成了「不限制平台」,导致报表串平台数据
现象:切到抖音,报表里显示的是小红书的数据。
根因是报告聚合里的一行真值判断:
scope = list(task_ids) if task_ids else None
空列表是假值,而空列表在这里的含义是「这个平台一个任务都没有」,不是「不限制平台」。
于是 platform=dy 且抖音还没有任务时,_resolve_scope 返回的 [] 被翻译成了 None,
聚合范围从「抖音的任务」变成了**全部任务** —— 小红书的数字就这么显示在了抖音页面上。
顺带 task_ids 也回成 None,界面会显示成「全部任务」。
改成 `is not None`。空列表进去就让 in_([]) 恒假,结果为空,这才是对的。
排查时把所有同类写法过了一遍,只有这一处错,其余(service.py 的 10 处作用域judgement、
_resolve_scope、export)用的都是 `is not None`。
测试:新增两条,并且**验证过它们在修复前会红**(失败信息就是 assert 42 == 0 ——
查一个没有任何任务的平台,却返回了小红书那条作品的 42 个赞)。
同时修掉一条空跑的测试:test_a_platform_with_no_tasks_yields_empty_not_everything
原先种了任务却没有作品/指标数据,于是过滤生效与否结果都是 0,什么都测不出来 ——
这正是这个 bug 能活下来的原因。现在它会真的塞一条作品+快照进去,并在末尾断言
「小红书自己的报表看得到那条数据」,用来证明前面那两个 0 是过滤出来的而不是没数据。
This commit is contained in:
@@ -135,7 +135,11 @@ async def build_report(
|
||||
start_ms, _ = day_bounds(start_day)
|
||||
_, end_ms = day_bounds(end_day)
|
||||
|
||||
scope = list(task_ids) if task_ids else None
|
||||
# 必须是 `is not None`,不能写 `if task_ids` —— **空列表是假值**,而空列表在这里
|
||||
# 的含义是「这个平台一个任务都没有」,不是「不限制平台」。用真值判断的话,
|
||||
# 切到一个还没有任务的平台,报表会把**所有**任务的数据聚合出来(看起来就是
|
||||
# 「抖音的报表里全是小红书的数据」)。
|
||||
scope = list(task_ids) if task_ids is not None else None
|
||||
days = iter_days(start_day, end_day)
|
||||
|
||||
# Fetch every snapshot up to the range end: the delta on the first day needs
|
||||
|
||||
@@ -184,9 +184,9 @@ async def db():
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def _seed_task(db: AsyncSession, name: str) -> MonitorTask:
|
||||
async def _seed_task(db: AsyncSession, name: str, platform: str = "xhs") -> MonitorTask:
|
||||
task = MonitorTask(
|
||||
name=name, platform="xhs", mode=MODE_CREATOR, enabled=True,
|
||||
name=name, platform=platform, mode=MODE_CREATOR, enabled=True,
|
||||
interval_minutes=60, max_notes_count=20, enable_comments=True,
|
||||
max_comments_count=50, run_timeout_seconds=3600,
|
||||
notify_enabled=False, created_at=0, updated_at=0,
|
||||
@@ -263,6 +263,25 @@ class TestBuildReport:
|
||||
assert result["totals"]["liked_count_delta"] == 30
|
||||
assert result["task_ids"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_empty_selection_is_not_the_same_as_no_filter(self, db):
|
||||
"""空列表 ≠ 不限制。
|
||||
|
||||
``_resolve_scope`` 在「这个平台一个任务都没有」时返回**空列表**。如果按真值
|
||||
处理(``if task_ids``),报表就会退化成「不限制平台」,把**所有**任务的数据
|
||||
聚合进来 —— 现象就是切到抖音,报表里却全是小红书的数据。
|
||||
"""
|
||||
other = await _seed_task(db, "xhs task", platform="xhs")
|
||||
await _seed_note_with_metrics(db, other, "n1", [(_ms(2026, 1, 10, 10), 999)])
|
||||
await db.commit()
|
||||
|
||||
result = await build_report(db, [], date(2026, 1, 10), date(2026, 1, 10))
|
||||
|
||||
assert result["totals"]["liked_count_delta"] == 0
|
||||
assert result["note_count"] == 0
|
||||
# 空列表要原样透出去;None 在 API 里的意思是「全部任务」,两者不能混。
|
||||
assert result["task_ids"] == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_baseline_from_before_the_range_is_used(self, db):
|
||||
"""Growth is measured against the last value before the window opens."""
|
||||
|
||||
+44
-3
@@ -21,13 +21,15 @@
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from tools.time_util import get_current_timestamp
|
||||
|
||||
from api.main import app
|
||||
from api.monitor import adapters
|
||||
from api.monitor import db as monitor_db
|
||||
from api.monitor import platforms
|
||||
from api.monitor.models import MonitorTask
|
||||
from api.monitor.models import MonitorNote, MonitorNoteMetric, MonitorTask
|
||||
|
||||
XHS_TARGET = "5f58bd990000000001003753"
|
||||
|
||||
@@ -44,6 +46,33 @@ async def client(tmp_path):
|
||||
await monitor_db.dispose_engine()
|
||||
|
||||
|
||||
async def _seed_note_with_one_snapshot(task_name: str) -> None:
|
||||
"""给某个任务塞一条作品和一次指标快照。
|
||||
|
||||
过滤类测试**必须有真数据**才有意义 —— 库里空着的话,过滤有没有生效结果都是 0,
|
||||
测试就变成了空跑(这个坑踩过一次:一个报表串数据的 bug 因此没被拦住)。
|
||||
"""
|
||||
async with monitor_db.get_session() as session:
|
||||
task = await session.scalar(select(MonitorTask).where(MonitorTask.name == task_name))
|
||||
now = get_current_timestamp()
|
||||
session.add(
|
||||
MonitorNote(
|
||||
task_id=task.id, note_id="seed-note", title="seed", note_url="",
|
||||
cover="", creator_hash="", source_kind="", published_at=None,
|
||||
first_seen_run_id=1, first_seen_at=now,
|
||||
last_seen_run_id=1, last_seen_at=now,
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
MonitorNoteMetric(
|
||||
task_id=task.id, note_id="seed-note", run_id=1, captured_at=now,
|
||||
liked_count=42, comment_count=0, collected_count=0, share_count=0,
|
||||
raw_liked_count="42", raw_comment_count="0",
|
||||
raw_collected_count="0", raw_share_count="0",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestCapabilityMatrix:
|
||||
@pytest.mark.asyncio
|
||||
async def test_matrix_is_exposed_to_the_ui(self, client):
|
||||
@@ -191,8 +220,15 @@ class TestPlatformScoping:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_platform_with_no_tasks_yields_empty_not_everything(self, client):
|
||||
"""An empty task set must not degrade into "no filter"."""
|
||||
"""空的任务集合不能退化成「不加过滤」。
|
||||
|
||||
**这里必须真的有数据。** 没有数据时,过滤生效与否结果都是 0 —— 这条测试原先
|
||||
就栽在这个空跑上,所以没能拦下一个报表串数据的 bug(切到抖音,报表里却出现
|
||||
小红书的数据)。最后那段「小红书自己的报表看得到」就是为了证明这些数据确实
|
||||
存在、上面那两个 0 是过滤出来的。
|
||||
"""
|
||||
await self._seed_two_platforms(client)
|
||||
await _seed_note_with_one_snapshot("小红书任务")
|
||||
|
||||
body = (await client.get("/api/monitor/notes", params={"platform": "bili"})).json()
|
||||
assert body["notes"] == []
|
||||
@@ -203,6 +239,11 @@ class TestPlatformScoping:
|
||||
assert report["totals"]["liked_count_delta"] == 0
|
||||
assert report["note_count"] == 0
|
||||
|
||||
xhs = (
|
||||
await client.get("/api/monitor/report", params={"platform": "xhs"})
|
||||
).json()
|
||||
assert xhs["note_count"] == 1
|
||||
|
||||
|
||||
class TestPerPlatformSettings:
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user