Files
trumpsignal-backend/tests/test_posts_paged.py
T
k 54884f3e24 KOL feeds: fix dead/blocked sources, drop stale feeds (29→25)
Feed-health pass over KOL_FEEDS:
- raoulpal: stale Substack (last 2024-05) → Real Vision podcast feed
- dampedspring: paywalled (0 entries) → free "Damped Spring 101" Substack
- unchained: Cloudflare 403 → canonical Megaphone podcast feed
- lynalden: Cloudflare 202 → FeedBurner mirror
- glassnode: recovered via httpx http2=True (was 403 on HTTP/1.1)
- browser User-Agent + Accept headers on feed fetch
- removed dead feeds with no active replacement: placeholder,
  dragonfly, niccarter, eugene
- pin h2==4.3.0 (required by http2=True)

All 25 remaining feeds verified fetching real body content; newest
post per feed ≤88d. Bundles in-flight KOL-module work already in the
working tree (kol_x ingest, migration 027, tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:55:16 +08:00

233 lines
8.1 KiB
Python

from __future__ import annotations
from datetime import datetime, timedelta
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.api.posts import router as posts_router
from app.database import get_db
from app.models import Base, Post
@pytest.mark.asyncio
async def test_posts_paged_filters_and_counts():
engine = create_async_engine("sqlite+aiosqlite:///:memory:", future=True)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
now = datetime(2026, 5, 30, 12, 0, 0)
async with session_factory() as session:
session.add_all([
Post(
external_id="truth-buy",
text="Bullish signal",
source="truth",
published_at=now,
sentiment="bullish",
signal="buy",
ai_confidence=91,
ai_reasoning="AI saw upside",
relevant=True,
),
Post(
external_id="truth-short",
text="Bearish signal",
source="truth",
published_at=now - timedelta(minutes=1),
sentiment="bearish",
signal="short",
ai_confidence=88,
ai_reasoning="AI saw downside",
relevant=True,
),
Post(
external_id="truth-hold",
text="Neutral but scored",
source="truth",
published_at=now - timedelta(minutes=2),
sentiment="neutral",
signal="hold",
ai_confidence=45,
ai_reasoning="No trade edge",
relevant=True,
),
Post(
external_id="truth-noise",
text="Off-topic golf post",
source="truth",
published_at=now - timedelta(minutes=3),
sentiment="neutral",
signal=None,
ai_confidence=0,
ai_reasoning=None,
relevant=False,
),
Post(
external_id="macro-buy",
text="Macro buy",
source="btc_bottom_reversal",
published_at=now - timedelta(minutes=4),
sentiment="bullish",
signal="buy",
ai_confidence=97,
ai_reasoning="Macro setup",
relevant=True,
),
Post(
external_id="archive-breakout",
text="Legacy breakout signal",
source="breakout",
published_at=now - timedelta(minutes=5),
sentiment="bullish",
signal="buy",
ai_confidence=73,
ai_reasoning="Old scanner",
relevant=True,
),
Post(
external_id="archive-sma",
text="Legacy sma reclaim signal",
source="sma_reclaim",
published_at=now - timedelta(minutes=6),
sentiment="bullish",
signal="short",
ai_confidence=64,
ai_reasoning="Old scanner 2",
relevant=True,
),
])
await session.commit()
app = FastAPI()
app.include_router(posts_router, prefix="/api")
async def override_get_db():
async with session_factory() as session:
yield session
app.dependency_overrides[get_db] = override_get_db
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
res = await client.get("/api/posts-paged", params={"source": "truth", "limit": 10, "page": 1})
assert res.status_code == 200
data = res.json()
assert data["total"] == 4
assert len(data["items"]) == 4
assert data["counts"] == {
"all": 4,
"actionable": 2,
"buy": 1,
"short": 1,
"off_topic": 1,
}
assert data["source_counts"] == [{"source": "truth", "count": 4, "latest": "2026-05-30T12:00:00.000Z"}]
actionable = await client.get(
"/api/posts-paged",
params={"source": "truth", "signal": "actionable", "limit": 10, "page": 1},
)
assert actionable.status_code == 200
actionable_data = actionable.json()
assert actionable_data["total"] == 2
assert {item["signal"] for item in actionable_data["items"]} == {"buy", "short"}
assert actionable_data["counts"]["all"] == 4
scored_only = await client.get(
"/api/posts-paged",
params={"source": "truth", "ai_scored_only": "true", "limit": 10, "page": 1},
)
assert scored_only.status_code == 200
scored_data = scored_only.json()
assert scored_data["total"] == 3
assert all(item["ai_confidence"] > 0 or item["ai_reasoning"] for item in scored_data["items"])
assert scored_data["counts"] == {
"all": 3,
"actionable": 2,
"buy": 1,
"short": 1,
"off_topic": 1,
}
bearish_buy = await client.get(
"/api/posts-paged",
params={
"source": "truth",
"sentiment": "bearish",
"signal": "buy",
"limit": 10,
"page": 1,
},
)
assert bearish_buy.status_code == 200
bearish_buy_data = bearish_buy.json()
assert bearish_buy_data["total"] == 0
assert bearish_buy_data["counts"] == {
"all": 1,
"actionable": 1,
"buy": 0,
"short": 1,
"off_topic": 0,
}
archive = await client.get(
"/api/posts-paged",
params={
"archive_only": "true",
"limit": 10,
"page": 1,
},
)
assert archive.status_code == 200
archive_data = archive.json()
assert archive_data["total"] == 2
assert len(archive_data["items"]) == 2
assert {item["source"] for item in archive_data["items"]} == {"breakout", "sma_reclaim"}
assert archive_data["source_counts"] == [
{"source": "breakout", "count": 1, "latest": "2026-05-30T11:55:00.000Z"},
{"source": "sma_reclaim", "count": 1, "latest": "2026-05-30T11:54:00.000Z"},
]
# Regression: selecting one archive source via source_in must narrow the
# paged items/total, but source_counts (the chip bar) must STILL list
# every archived source so the UI can offer a way back to "all".
archive_one = await client.get(
"/api/posts-paged",
params={
"archive_only": "true",
"source_in": "breakout",
"limit": 10,
"page": 1,
},
)
assert archive_one.status_code == 200
archive_one_data = archive_one.json()
assert archive_one_data["total"] == 1
assert {item["source"] for item in archive_one_data["items"]} == {"breakout"}
# Chip bar is NOT collapsed to the selected source.
assert archive_one_data["source_counts"] == [
{"source": "breakout", "count": 1, "latest": "2026-05-30T11:55:00.000Z"},
{"source": "sma_reclaim", "count": 1, "latest": "2026-05-30T11:54:00.000Z"},
]
archive_compat = await client.get(
"/api/posts-paged",
params={
"archive_only": "true",
"source_not_in": "truth",
"limit": 10,
"page": 1,
},
)
assert archive_compat.status_code == 200
archive_compat_data = archive_compat.json()
assert archive_compat_data["total"] == 2
assert {item["source"] for item in archive_compat_data["items"]} == {"breakout", "sma_reclaim"}
await engine.dispose()