SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
15.5 KB · 270 lines python
Raw Blame History
1"""API contract smoke tests against the live local database (docs/API.md). Read-only except for the `/views` beacon.2Counts are never asserted as fixed numbers: connectors add data while the suite runs."""3from __future__ import annotations45from collections.abc import AsyncIterator67import pytest8from httpx import ASGITransport, AsyncClient910from aiatlas import db11from aiatlas.api.main import app12from aiatlas.config import settings13from aiatlas.services import cache1415ADMIN = {"x-aia-admin-token": settings.admin_token or "dev-admin-token"}16MODEL = "claude-opus-5"17MODEL_B = "claude-sonnet-5"181920@pytest.fixture21async def client() -> AsyncIterator[AsyncClient]:22    """pytest-asyncio runs each test in its own loop: pools must not outlive the loop that created them."""23    await cache.cache_invalidate()24    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:25        yield c26    await cache.close()27    await db.dispose()282930async def test_health(client: AsyncClient) -> None:31    for path in ("/health", "/api/v1/health"):32        r = await client.get(path)33        assert r.status_code == 20034        body = r.json()35        assert body["status"] in ("ok", "degraded") and body["db"] is True36        assert {"version", "redis", "llm", "time"} <= set(body)373839async def test_stats_keys_and_live(client: AsyncClient) -> None:40    r = await client.get("/api/v1/stats")41    assert r.status_code == 20042    s = r.json()43    for key in ("entities", "entities_total", "sources", "connectors", "documents", "snapshots", "claims", "claims_current", "relations", "change_events",44                "change_events_24h", "benchmark_results", "prices_current", "prices_total", "review_pending", "llm_jobs", "archive", "computed_at"):45        assert key in s, key46    assert s["entities_total"] == sum(s["entities"].values()) > 047    assert s["entities"].get("model", 0) > 048    for t in ("model", "company", "paper", "provider", "benchmark", "hardware", "framework", "dataset", "tool", "repository"):49        assert t in s["entities"], t  # zero-filled for the homepage50    assert {"raw_bytes", "raw_files", "text_bytes", "text_files"} <= set(s["archive"])51    hist = await client.get("/api/v1/stats/history?days=30")52    assert hist.status_code == 200 and "items" in hist.json()535455async def test_search_returns_claude_models(client: AsyncClient) -> None:56    r = await client.get("/api/v1/search", params={"q": "claude", "limit": 10})57    assert r.status_code == 20058    body = r.json()59    assert body["total"] > 0 and body["items"]60    assert body["query"]["text"] == "claude"61    assert all("rank" in it for it in body["items"])62    assert any(it["entity_type"] == "model" and it["slug"].startswith("claude") for it in body["items"])63    sug = await client.get("/api/v1/search/suggest", params={"q": "cla"})64    assert sug.status_code == 200 and sug.json()["items"]65    assert {"id", "entity_type", "slug", "name", "organization_name"} <= set(sug.json()["items"][0])66    assert (await client.get("/api/v1/search", params={"q": ""})).status_code == 400676869async def test_models_filters(client: AsyncClient) -> None:70    r = await client.get("/api/v1/models", params={"openness": "proprietary", "limit": 200})71    assert r.status_code == 20072    body = r.json()73    assert {"items", "total", "limit", "offset"} <= set(body) and body["items"]74    assert all(it["attributes"].get("openness") == "proprietary" for it in body["items"])75    r = await client.get("/api/v1/models", params={"min_context": 500000, "limit": 200})76    assert r.status_code == 20077    items = r.json()["items"]78    assert items and all(int(it["attributes"]["context_length"]) >= 500000 for it in items)79    r = await client.get("/api/v1/models", params={"facets": 1, "limit": 1})80    assert r.status_code == 20081    facets = r.json()["facets"]82    assert {"organizations", "openness", "modalities", "families", "years", "licenses", "status"} <= set(facets)83    assert any(o["slug"] == "anthropic" for o in facets["organizations"])84    assert (await client.get("/api/v1/models", params={"sort": "nope"})).status_code == 40085    assert (await client.get("/api/v1/models", params={"limit": 999})).status_code == 422868788async def test_entity_detail_blocks(client: AsyncClient) -> None:89    r = await client.get(f"/api/v1/entities/{MODEL}")90    assert r.status_code == 20091    d = r.json()92    assert d["slug"] == MODEL and d["entity_type"] == "model"93    for block in ("attributes", "provenance", "aliases", "identifiers", "relations", "sources", "timeline", "prices", "price_history", "results", "lineage",94                  "providers", "quality", "counts"):95        assert block in d, block96    assert d["provenance"] and all({"tier", "confidence", "extractor", "observed_at"} <= set(v) for v in d["provenance"].values())97    assert any(v.get("source_name") for v in d["provenance"].values() if v.get("source_id"))98    assert d["status"] in ("active", "preview", "deprecated", "retired", "announced", "limited-availability", "unknown")99    assert d["prices"] and d["prices"][0]["provider"]["slug"] and d["prices"][0]["input_per_mtok"] is not None100    assert d["timeline"] and any(e["event_type"] == "NEW_MODEL" for e in d["timeline"])101    assert d["sources"] and {"url", "tier", "doc_type", "snapshots"} <= set(d["sources"][0])102    assert d["organization"]["slug"] == "anthropic"103    assert {"ancestors", "descendants", "quantizations"} == set(d["lineage"])104    # type-scoped aliases105    assert (await client.get(f"/api/v1/models/{MODEL}")).status_code == 200106    assert (await client.get(f"/api/v1/companies/{MODEL}")).status_code == 404107    assert (await client.get("/api/v1/entities/definitely-not-an-entity")).status_code == 404108    # resolution by id109    by_id = await client.get(f"/api/v1/entities/{d['id']}")110    assert by_id.status_code == 200 and by_id.json()["slug"] == MODEL111112113async def test_company_detail(client: AsyncClient) -> None:114    r = await client.get("/api/v1/companies/anthropic")115    assert r.status_code == 200116    d = r.json()117    assert d["entity_type"] in ("company", "organization", "lab", "university")118    assert d["models"]["total"] > 0 and d["models"]["items"][0]["entity_type"] == "model"119    assert d["timeline"]  # includes events of the models it develops120121122async def test_entity_subresources(client: AsyncClient) -> None:123    tl = await client.get(f"/api/v1/entities/{MODEL}/timeline", params={"limit": 5})124    assert tl.status_code == 200 and "items" in tl.json()125    hist = await client.get(f"/api/v1/entities/{MODEL}/history", params={"property": "context_length"})126    assert hist.status_code == 200 and hist.json()["items"] and hist.json()["items"][0]["property"] == "context_length"127    graph = await client.get(f"/api/v1/entities/{MODEL}/graph", params={"depth": 1})128    assert graph.status_code == 200 and graph.json()["nodes"] and "edges" in graph.json()129    src = await client.get(f"/api/v1/entities/{MODEL}/sources")130    assert src.status_code == 200 and src.json()["items"]131    rel = await client.get(f"/api/v1/entities/{MODEL}/related", params={"limit": 5})132    assert rel.status_code == 200 and len(rel.json()["items"]) <= 5133134135async def test_asof(client: AsyncClient) -> None:136    r = await client.get(f"/api/v1/entities/{MODEL}/asof", params={"date": "2020-01-01"})137    assert r.status_code == 200138    body = r.json()139    assert body["existed"] is False and body["attributes"] == {} and body["claims"] == []140    now = await client.get(f"/api/v1/entities/{MODEL}/asof", params={"date": "2999-12-31"})141    assert now.status_code == 200 and now.json()["existed"] is True and now.json()["attributes"]142    assert (await client.get(f"/api/v1/entities/{MODEL}/asof", params={"date": "not-a-date"})).status_code == 400143144145async def test_changes_pagination(client: AsyncClient) -> None:146    first = await client.get("/api/v1/changes", params={"limit": 3})147    assert first.status_code == 200148    body = first.json()149    assert {"items", "total", "limit", "offset"} <= set(body) and len(body["items"]) == 3 and body["total"] >= 3150    assert all(e["event_type"] != "DOCUMENT_CHANGED" for e in body["items"])151    ev = body["items"][0]152    assert {"id", "event_type", "category", "summary", "importance", "observed_at", "entity", "meta"} <= set(ev)153    cursor = body["next_before"]154    assert cursor == body["items"][-1]["observed_at"]155    second = await client.get("/api/v1/changes", params={"limit": 3, "before": cursor})156    assert second.status_code == 200157    ids1 = {e["id"] for e in body["items"]}158    assert all(e["id"] not in ids1 for e in second.json()["items"])159    assert all(e["observed_at"] < cursor for e in second.json()["items"])160    daily = await client.get("/api/v1/changes/daily")161    assert daily.status_code == 200 and {"date", "counts", "sections", "new_models"} <= set(daily.json())162    cats = await client.get("/api/v1/changes/categories", params={"days": 30})163    assert cats.status_code == 200 and "items" in cats.json()164165166async def test_compare(client: AsyncClient) -> None:167    r = await client.get("/api/v1/compare", params={"ids": f"{MODEL},{MODEL_B}"})168    assert r.status_code == 200169    body = r.json()170    assert body["entity_type"] == "model" and len(body["items"]) == 2171    keys = {d["key"] for d in body["dimensions"]}172    assert {"parameter_count", "context_length", "openness", "release_date", "best_input_per_mtok"} <= keys173    for item in body["items"]:174        assert {"entity", "values", "provenance", "prices", "results"} <= set(item)175        assert item["values"]["context_length"] is not None176    assert (await client.get("/api/v1/compare", params={"ids": MODEL})).status_code == 400177    assert (await client.get("/api/v1/compare", params={"ids": f"{MODEL},anthropic"})).status_code == 400178179180async def test_listings_and_misc(client: AsyncClient) -> None:181    for path in ("/api/v1/companies?facets=1", "/api/v1/papers", "/api/v1/providers", "/api/v1/prices", "/api/v1/prices/index?days=14", "/api/v1/benchmarks",182                 "/api/v1/hardware", "/api/v1/hardware/fit?memory_gb=24", "/api/v1/explore/types", "/api/v1/explore/hardware", "/api/v1/timeline?limit=10",183                 "/api/v1/diff?a=2026-01-01&b=2026-12-31", "/api/v1/sources", "/api/v1/methodology", "/api/v1/trending", "/api/v1/sitemap?limit=5",184                 f"/api/v1/prices/history?model={MODEL}"):185        r = await client.get(path)186        assert r.status_code == 200, (path, r.text[:200])187    prices = (await client.get("/api/v1/prices", params={"model": MODEL})).json()188    assert prices["items"] and prices["items"][0]["model"]["slug"] == MODEL189    meth = (await client.get("/api/v1/methodology")).json()190    assert meth["metrics"] and {"key", "label", "version", "description"} <= set(meth["metrics"][0])191    assert all({"event_type", "label", "importance", "count"} <= set(t) for t in meth["event_types"])192    assert next(t for t in meth["event_types"] if t["event_type"] == "NEW_MODEL")["label"] == "New model"193    srcs = (await client.get("/api/v1/sources")).json()["items"]194    linked = [c for s in srcs for c in s["connectors"]]195    assert linked and {"name", "label", "health", "last_success_at", "interval_seconds"} <= set(linked[0])196    fit = (await client.get("/api/v1/hardware/fit", params={"memory_gb": 24})).json()197    assert fit["estimated"] is True and fit["assumptions"] and "items" in fit198    assert (await client.get("/api/v1/hardware/fit", params={"memory_gb": 24, "quant": "2bit"})).status_code == 400199    assert (await client.get("/api/v1/explore/nonsense")).status_code == 404200    assert (await client.get("/api/v1/api-keys/me")).status_code == 401201    view = await client.post("/api/v1/views", json={"path": "/models/claude-opus-5"})202    assert view.status_code == 200 and view.json() == {"ok": True}203    assert (await client.post("/api/v1/views", json={"path": "/x"})).status_code == 429  # 1 req/s/IP204205206async def test_cache_roundtrip_is_stable(client: AsyncClient) -> None:207    a = (await client.get("/api/v1/models", params={"limit": 3})).json()208    b = (await client.get("/api/v1/models", params={"limit": 3})).json()209    assert a == b210211212async def test_admin_auth(client: AsyncClient) -> None:213    assert (await client.get("/api/v1/admin/overview")).status_code == 401214    assert (await client.get("/api/v1/admin/overview", headers={"x-aia-admin-token": "wrong"})).status_code == 401215    r = await client.get("/api/v1/admin/overview", headers=ADMIN)216    assert r.status_code == 200217    body = r.json()218    assert {"stats", "queue", "heartbeats", "connectors", "review_pending", "recent_errors", "llm", "archive"} <= set(body)219    saved = settings.admin_token220    settings.admin_token = ""221    try:222        assert (await client.get("/api/v1/admin/overview", headers=ADMIN)).status_code == 503223    finally:224        settings.admin_token = saved225226227async def test_merge_entities_rolled_back(client: AsyncClient) -> None:228    """Exercise the curation SQL end-to-end on live rows, then roll back — the database is left untouched."""229    from aiatlas.db import engine, fetch_one, fetch_val230    from aiatlas.services.merge import merge_entities231232    async with engine().connect() as conn:233        trans = await conn.begin()234        try:235            src = await fetch_one(conn, "select id from entities where slug = :s", s=MODEL_B)236            dst = await fetch_one(conn, "select id from entities where slug = :s", s=MODEL)237            assert src and dst238            res = await merge_entities(conn, src["id"], dst["id"])239            assert res["target_id"] == dst["id"] and res["moved"]["claims"] > 0240            merged = await fetch_one(conn, "select status, merged_into from entities where id = :id", id=src["id"])241            assert merged["status"] == "merged" and merged["merged_into"] == dst["id"]242            assert await fetch_val(conn, "select count(*) from claims where entity_id = :id", id=src["id"]) == 0243            assert await fetch_val(conn, "select count(*) from entity_aliases where entity_id = :t and kind = 'former_name'", t=dst["id"]) >= 1244            with pytest.raises(ValueError):245                await merge_entities(conn, src["id"], dst["id"])  # already merged246        finally:247            await trans.rollback()248    async with engine().connect() as conn:249        assert (await fetch_one(conn, "select status from entities where slug = :s", s=MODEL_B))["status"] != "merged"250251252async def test_admin_read_routes(client: AsyncClient) -> None:253    review = await client.get("/api/v1/admin/review", headers=ADMIN)254    assert review.status_code == 200 and {"items", "total", "by_kind"} <= set(review.json())255    for path in ("/api/v1/admin/connectors", "/api/v1/admin/runs?limit=2", "/api/v1/admin/errors?limit=2", "/api/v1/admin/documents?limit=2", "/api/v1/admin/jobs",256                 "/api/v1/admin/llm-jobs?limit=2", "/api/v1/admin/entities/duplicates?type=model", "/api/v1/admin/infrastructure"):257        r = await client.get(path, headers=ADMIN)258        assert r.status_code == 200, (path, r.text[:200])259    docs = (await client.get("/api/v1/admin/documents?limit=1", headers=ADMIN)).json()260    if docs["items"]:261        doc = (await client.get(f"/api/v1/admin/documents/{docs['items'][0]['id']}", headers=ADMIN)).json()262        assert "snapshots" in doc263        if doc["snapshots"]:264            snap = (await client.get(f"/api/v1/admin/snapshots/{doc['snapshots'][0]['id']}", headers=ADMIN)).json()265            assert "raw_path" not in snap and "text_path" not in snap266            assert {"structured", "diff", "text", "claims"} <= set(snap)267            assert snap["text"] is None or len(snap["text"]) <= 20 * 1024268    assert (await client.post("/api/v1/admin/connectors/does-not-exist/run", headers=ADMIN, json={})).status_code == 404269    assert (await client.get("/api/v1/admin/review/nope", headers=ADMIN)).status_code in (404, 405)270