Reports: the LLM reliably used company_enrichment for prose fields but inconsistently populated the parallel Finding-list/string-list fields from the same evidence, even with progressively more explicit prompting. Add a code-level backfill (products, recent developments, financial signals, strategic initiatives, regulatory signals, risks/opportunities mirrored from SWOT, unknowns, monitoring recommendations) that only ever fills in what the model left empty, never overwrites what it produced. Enrichment tab: reorder sections (Products/Recent updates before Customers/Competitors) and add a per-section "Refresh" button that re-fetches just one of NinjaPear's six independent per-company endpoints when it came back empty - confirmed live that a data-coverage gap (e.g. Amazon returning no products) is real provider behavior, not a bug. Auth: the first account registered on a deployment with zero existing admins is now auto-promoted to admin, closing the chicken-and-egg gap where the only path to admin access was direct DB access. Self-heals if the last admin ever deletes their account. Also bumps nginx's proxy_read_timeout for api.ciagent.org to cover the enrichment refresh's synchronous funding-endpoint call (up to 5 minutes per NinjaPear's docs). Co-Authored-By: Claude Sonnet 5 <[email protected]>
113 lines
4.2 KiB
Python
113 lines
4.2 KiB
Python
"""Shared pytest fixtures.
|
|
|
|
Tests always run with LLM_PROVIDER=mock, SEARCH_PROVIDER=mock, a throwaway
|
|
SQLite DB, and Celery in eager mode - never against a paid provider or a
|
|
real network target.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
os.environ.setdefault("APP_ENV", "test")
|
|
os.environ.setdefault("AUTH_MODE", "jwt")
|
|
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_ciagent.db")
|
|
os.environ.setdefault("REDIS_URL", "redis://localhost:6379/15")
|
|
os.environ.setdefault("LLM_PROVIDER", "mock")
|
|
os.environ.setdefault("SEARCH_PROVIDER", "mock")
|
|
os.environ.setdefault("CELERY_TASK_ALWAYS_EAGER", "true")
|
|
os.environ.setdefault("JWT_SECRET", "test-secret-please-change-32-characters")
|
|
# No artificial per-domain delay in tests - the collector tests hit many
|
|
# distinct mocked hostnames and shouldn't pay the real-world crawl-politeness cost.
|
|
os.environ.setdefault("SCRAPER_DOMAIN_DELAY_SECONDS", "0")
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.core.config import Settings, get_settings
|
|
from app.db.base import Base
|
|
from app.db.session import get_engine, get_sessionmaker
|
|
from app.main import app
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def _setup_database():
|
|
# The DB is a throwaway file-based SQLite DB (aiosqlite doesn't support
|
|
# ":memory:" across the multiple connections a test session opens), so it
|
|
# must be deleted up front - otherwise companies/runs left behind by a
|
|
# previous test session accumulate and pollute count-based assertions
|
|
# (e.g. the scheduler "how many companies got enqueued" tests).
|
|
db_path = Path("test_ciagent.db")
|
|
db_path.unlink(missing_ok=True)
|
|
|
|
async def _create() -> None:
|
|
engine = get_engine()
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
|
|
# auth_service.register() auto-promotes the first-ever registration
|
|
# to admin (see its docstring) so a fresh deployment always has a
|
|
# bootstrap path to admin access. Without this seed row, whichever
|
|
# test happens to register a "plain" user first in this shared
|
|
# session-wide DB would non-deterministically become an admin,
|
|
# breaking every non-admin-should-get-403 test depending on
|
|
# execution order. Seeding one admin up front keeps that dedicated
|
|
# to test_first_registered_user_becomes_admin (which starts from a
|
|
# genuinely empty users table), while every other test's "plain"
|
|
# registration behaves exactly as before.
|
|
from app.models.user import User
|
|
|
|
async with get_sessionmaker()() as session:
|
|
session.add(
|
|
User(
|
|
email="[email protected]",
|
|
password_hash=None,
|
|
display_name="Seed Admin",
|
|
timezone="UTC",
|
|
is_admin=True,
|
|
email_verified=True,
|
|
)
|
|
)
|
|
await session.commit()
|
|
|
|
asyncio.run(_create())
|
|
yield
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def settings() -> Settings:
|
|
return get_settings()
|
|
|
|
|
|
@pytest.fixture()
|
|
def client() -> TestClient:
|
|
with TestClient(app) as c:
|
|
yield c
|
|
|
|
|
|
@pytest.fixture()
|
|
async def db_session():
|
|
"""Raw AsyncSession for tests that exercise services/repositories
|
|
directly rather than through the HTTP API."""
|
|
session_factory = get_sessionmaker()
|
|
async with session_factory() as session:
|
|
yield session
|
|
|
|
|
|
@pytest.fixture()
|
|
def local_mode_client(settings: Settings) -> TestClient:
|
|
"""A client with AUTH_MODE forced to "local" via dependency override,
|
|
independent of whatever AUTH_MODE the rest of the suite runs under - and
|
|
with its TCP peer set to loopback, since the local-dev-user bypass
|
|
(app.auth.dependencies.get_current_user) only fires for a genuinely
|
|
local request, not merely AUTH_MODE=local. Starlette's TestClient
|
|
defaults to a fake ("testclient", 50000) peer otherwise."""
|
|
local_settings = settings.model_copy(update={"auth_mode": "local"})
|
|
app.dependency_overrides[get_settings] = lambda: local_settings
|
|
with TestClient(app, client=("127.0.0.1", 51234)) as c:
|
|
yield c
|
|
app.dependency_overrides.pop(get_settings, None)
|