101 lines
3 KiB
Python
101 lines
3 KiB
Python
|
|
"""End-to-end pipeline test — mocked PRAW + respx-mocked LLM + in-memory DB."""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from collections.abc import AsyncIterator
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from datetime import datetime
|
||
|
|
from decimal import Decimal
|
||
|
|
from unittest.mock import AsyncMock, MagicMock
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
import pytest
|
||
|
|
import respx
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from fire_planner.db import FireExample
|
||
|
|
from fire_planner.examples.cli import ingest_subreddit
|
||
|
|
|
||
|
|
LLAMA_URL = "http://llama-cpp.llama-cpp.svc.cluster.local:8000/v1/chat/completions"
|
||
|
|
CLAUDE_URL = "http://claude-agent-service.claude-agent.svc.cluster.local:8080/v1/chat/completions"
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class _FakeSub:
|
||
|
|
id: str
|
||
|
|
title: str
|
||
|
|
selftext: str
|
||
|
|
permalink: str
|
||
|
|
created_utc: float
|
||
|
|
|
||
|
|
|
||
|
|
def _async_iter(items: list[_FakeSub]) -> AsyncIterator[_FakeSub]:
|
||
|
|
async def _gen() -> AsyncIterator[_FakeSub]:
|
||
|
|
for it in items:
|
||
|
|
yield it
|
||
|
|
return _gen()
|
||
|
|
|
||
|
|
|
||
|
|
@respx.mock
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_ingest_subreddit_end_to_end(session: AsyncSession) -> None:
|
||
|
|
fakes = [
|
||
|
|
_FakeSub(
|
||
|
|
id="ok1",
|
||
|
|
title="FIRE at 38 in Manila",
|
||
|
|
selftext="Net worth £1m, family of 3, retired last year",
|
||
|
|
permalink="/r/ExpatFIRE/comments/ok1/",
|
||
|
|
created_utc=datetime(2026, 1, 1).timestamp(),
|
||
|
|
),
|
||
|
|
_FakeSub( # filter should drop this — no money signal
|
||
|
|
id="drop1",
|
||
|
|
title="Thinking about moving to Lisbon",
|
||
|
|
selftext="No specifics yet",
|
||
|
|
permalink="/r/ExpatFIRE/comments/drop1/",
|
||
|
|
created_utc=datetime(2026, 1, 2).timestamp(),
|
||
|
|
),
|
||
|
|
]
|
||
|
|
mock_subreddit = MagicMock()
|
||
|
|
mock_subreddit.top = MagicMock(return_value=_async_iter(fakes))
|
||
|
|
mock_reddit = MagicMock()
|
||
|
|
mock_reddit.subreddit = AsyncMock(return_value=mock_subreddit)
|
||
|
|
|
||
|
|
payload = {
|
||
|
|
"country": "Philippines",
|
||
|
|
"city": "Manila",
|
||
|
|
"portfolio_native": 1000000,
|
||
|
|
"raw_currency": "GBP",
|
||
|
|
"age": 38,
|
||
|
|
"family_size": 3,
|
||
|
|
"fi_status": "FIRE",
|
||
|
|
"is_retired": True,
|
||
|
|
"confidence": 0.8,
|
||
|
|
}
|
||
|
|
respx.post(LLAMA_URL).respond(
|
||
|
|
200,
|
||
|
|
json={"choices": [{"message": {"content": json.dumps(payload)}}]},
|
||
|
|
)
|
||
|
|
|
||
|
|
fx_rates = {"GBP": Decimal("1"), "USD": Decimal("0.80")}
|
||
|
|
async with httpx.AsyncClient() as client:
|
||
|
|
n_inserted, n_skipped = await ingest_subreddit(
|
||
|
|
session,
|
||
|
|
mock_reddit,
|
||
|
|
sub="ExpatFIRE",
|
||
|
|
when="all",
|
||
|
|
limit=10,
|
||
|
|
llama_url=LLAMA_URL,
|
||
|
|
claude_url=CLAUDE_URL,
|
||
|
|
claude_bearer="t",
|
||
|
|
client=client,
|
||
|
|
fx_rates=fx_rates,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert n_inserted == 1
|
||
|
|
assert n_skipped == 1
|
||
|
|
rows = (await session.execute(select(FireExample))).scalars().all()
|
||
|
|
assert len(rows) == 1
|
||
|
|
assert rows[0].country == "Philippines"
|
||
|
|
assert rows[0].portfolio_gbp == Decimal("1000000.00")
|