HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
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