Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""Regression test for the collection + change-detection pipeline using the
|
||||
Acme Mobility Systems v1/v2 fixture HTML under tests/fixtures/acme_mobility/,
|
||||
read straight off disk and run through the real pipeline end-to-end - so a
|
||||
change to those fixtures, or a regression in the pipeline, breaks a test
|
||||
here rather than going unnoticed. No live network: respx mocks every HTTP
|
||||
call for a fictitious acme-mobility.example host."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.company import Company
|
||||
from app.models.enums import ChangeType, SourceType
|
||||
from app.models.monitor_configuration import MonitorConfiguration
|
||||
from app.repositories.source_repository import SnapshotRepository, SourceRepository
|
||||
from app.services import collection_service
|
||||
from app.services.change_detection_service import detect_change_for_source
|
||||
|
||||
_FIXTURE_ROOT = Path(__file__).resolve().parent.parent / "fixtures" / "acme_mobility"
|
||||
_HOST = "https://acme-mobility.example"
|
||||
_PAGES = ["about", "products", "careers", "press", "pricing"]
|
||||
|
||||
|
||||
def _page_html(version: str, page: str) -> str:
|
||||
return (_FIXTURE_ROOT / version / f"{page}.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_real_dns():
|
||||
with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]):
|
||||
yield
|
||||
|
||||
|
||||
async def _make_company(db_session) -> Company:
|
||||
company = Company(
|
||||
id=uuid.uuid4(),
|
||||
user_id=uuid.uuid4(),
|
||||
name="Acme Mobility Systems (Demo)",
|
||||
slug=f"acme-mobility-demo-{uuid.uuid4().hex[:6]}",
|
||||
official_website=f"{_HOST}/about",
|
||||
monitoring_focus="leadership changes, pricing, hiring, expansion",
|
||||
)
|
||||
db_session.add(company)
|
||||
db_session.add(MonitorConfiguration(company_id=company.id))
|
||||
await db_session.commit()
|
||||
result = await db_session.execute(
|
||||
select(Company)
|
||||
.where(Company.id == company.id)
|
||||
.options(
|
||||
selectinload(Company.aliases),
|
||||
selectinload(Company.competitors),
|
||||
selectinload(Company.enrichment),
|
||||
)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
def _mock_version(version: str):
|
||||
respx.get(f"{_HOST}/robots.txt").mock(return_value=httpx.Response(404))
|
||||
for page in _PAGES:
|
||||
respx.get(f"{_HOST}/{page}").mock(
|
||||
return_value=httpx.Response(200, html=_page_html(version, page))
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acme_v1_to_v2_produces_the_expected_change_types(db_session, settings):
|
||||
company = await _make_company(db_session)
|
||||
repo = SourceRepository(db_session)
|
||||
sources = {}
|
||||
for page in _PAGES:
|
||||
sources[page] = await repo.create(
|
||||
company_id=company.id,
|
||||
source_type=SourceType.CUSTOM_URL,
|
||||
name=f"Acme Mobility - {page.title()}",
|
||||
base_url=f"{_HOST}/{page}",
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
# Baseline run (v1): every source collects successfully, no prior
|
||||
# snapshot to diff against yet.
|
||||
with respx.mock:
|
||||
_mock_version("v1")
|
||||
for page in _PAGES:
|
||||
result = await collection_service.collect_source(
|
||||
db_session, settings, sources[page], company
|
||||
)
|
||||
assert result.status.value == "active", f"{page} baseline collection failed"
|
||||
|
||||
# Second run (v2): about/careers/press/pricing changed, products did not.
|
||||
with respx.mock:
|
||||
_mock_version("v2")
|
||||
for page in _PAGES:
|
||||
await collection_service.collect_source(db_session, settings, sources[page], company)
|
||||
|
||||
snapshot_repo = SnapshotRepository(db_session)
|
||||
changes: dict[str, ChangeType | None] = {}
|
||||
for page in _PAGES:
|
||||
current = await snapshot_repo.latest_for_source(sources[page].id)
|
||||
change = await detect_change_for_source(
|
||||
db_session, sources[page], company, current, uuid.uuid4()
|
||||
)
|
||||
changes[page] = change.change_type if change else None
|
||||
|
||||
assert changes["about"] == ChangeType.LEADERSHIP_CHANGE
|
||||
assert changes["pricing"] == ChangeType.PRICE_CHANGE
|
||||
assert changes["products"] is None # identical content -> hash short-circuit, no change
|
||||
assert changes["careers"] is not None # new job listing -> detected as a real change
|
||||
assert changes["press"] is not None # new press entry -> detected as a real change
|
||||
Reference in New Issue
Block a user