"""Tests for /api/leaderboard endpoint (diagnostics/leaderboard_routes.py). Locks behavior for the four slicings consumed by Argus + Oberon: - window: all_time | Nd | Nh - domain: per-domain filter - kind: person | agent | org | all - limit: pagination + has_more flag Regression coverage includes the AND-prefix SQL bug (commit 42d35d4): _parse_window returned clauses prefixed with 'AND ' which produced 'WHERE 1=1 AND AND ...' when joined into the WHERE clause via " AND ".join(...). """ import asyncio import json import sqlite3 from pathlib import Path import pytest # Skip whole file if aiohttp isn't available (matches test_activity_classify.py pattern) aiohttp = pytest.importorskip("aiohttp") # Make diagnostics/ importable import sys DIAG_ROOT = Path(__file__).parent.parent / "diagnostics" sys.path.insert(0, str(DIAG_ROOT)) from leaderboard_routes import ( # noqa: E402 _parse_window, handle_leaderboard, KIND_VALUES, LEADERBOARD_PUBLIC_PATHS, ) from aiohttp.test_utils import make_mocked_request # noqa: E402 # ─── Schema lifted from lib/db.py:138-209 (v25 minimum) ────────────────────── SCHEMA = """ CREATE TABLE contributors ( handle TEXT PRIMARY KEY, kind TEXT DEFAULT 'person', tier TEXT DEFAULT 'new', claims_merged INTEGER DEFAULT 0, sourcer_count INTEGER DEFAULT 0, extractor_count INTEGER DEFAULT 0, challenger_count INTEGER DEFAULT 0, synthesizer_count INTEGER DEFAULT 0, reviewer_count INTEGER DEFAULT 0, challenges_survived INTEGER DEFAULT 0, domains TEXT DEFAULT '[]', first_contribution TEXT, last_contribution TEXT ); CREATE TABLE contribution_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, handle TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'person', role TEXT NOT NULL, weight REAL NOT NULL, pr_number INTEGER NOT NULL, claim_path TEXT, domain TEXT, channel TEXT, timestamp TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE UNIQUE INDEX idx_ce_unique_claim ON contribution_events( handle, role, pr_number, claim_path ) WHERE claim_path IS NOT NULL; CREATE UNIQUE INDEX idx_ce_unique_pr ON contribution_events( handle, role, pr_number ) WHERE claim_path IS NULL; """ # ─── Fixtures ──────────────────────────────────────────────────────────────── @pytest.fixture def db_path(tmp_path): """Seeded pipeline.db with deterministic events. Cohort: - alice (person): 3 author events, 1 originator (recent 3d, internet-finance) - bob (person): 5 author events (older, 60d ago, ai-alignment) - carol (person): 1 author + 1 evaluator (today, internet-finance) - rio (agent): 4 author + 2 evaluator (mixed, internet-finance + grand-strategy) - leo (agent): 8 evaluator events (today, mixed domains) - cnbc (org): 2 originator events (legacy, before classifier moved orgs) - newhandle (no contributors row): 1 author event — tests LEFT JOIN COALESCE """ p = tmp_path / "pipeline.db" conn = sqlite3.connect(str(p)) conn.executescript(SCHEMA) contribs = [ ("alice", "person"), ("bob", "person"), ("carol", "person"), ("rio", "agent"), ("leo", "agent"), ("cnbc", "org"), # newhandle intentionally absent — tests LEFT JOIN ] for handle, kind in contribs: conn.execute( "INSERT INTO contributors (handle, kind) VALUES (?, ?)", (handle, kind), ) # (handle, role, weight, pr_number, claim_path, domain, timestamp) events = [ # alice — 3 author + 1 originator, recent (all >24h ago, all <7d) # Most-recent event at -2 days (not -1 days) so 24h window exclusion is # unambiguous and not subject to fixture-vs-query microsecond drift. ("alice", "author", 0.30, 100, None, "internet-finance", "now,-2 days"), ("alice", "author", 0.30, 101, None, "internet-finance", "now,-2 days"), ("alice", "author", 0.30, 102, None, "ai-alignment", "now,-3 days"), ("alice", "originator", 0.15, 103, "domains/internet-finance/x.md", "internet-finance", "now,-2 days"), # bob — 5 author, all 60d ago (outside 30d, inside all_time) ("bob", "author", 0.30, 200, None, "ai-alignment", "now,-60 days"), ("bob", "author", 0.30, 201, None, "ai-alignment", "now,-60 days"), ("bob", "author", 0.30, 202, None, "ai-alignment", "now,-61 days"), ("bob", "author", 0.30, 203, None, "ai-alignment", "now,-62 days"), ("bob", "author", 0.30, 204, None, "ai-alignment", "now,-63 days"), # carol — 1 author + 1 evaluator, today ("carol", "author", 0.30, 300, None, "internet-finance", "now"), ("carol", "evaluator", 0.05, 301, None, "internet-finance", "now"), # rio agent — 4 author + 2 evaluator ("rio", "author", 0.30, 400, None, "internet-finance", "now,-2 days"), ("rio", "author", 0.30, 401, None, "grand-strategy", "now,-2 days"), ("rio", "author", 0.30, 402, None, "internet-finance", "now,-2 days"), ("rio", "author", 0.30, 403, None, "internet-finance", "now,-2 days"), ("rio", "evaluator", 0.05, 404, None, "ai-alignment", "now,-2 days"), ("rio", "evaluator", 0.05, 405, None, "ai-alignment", "now,-2 days"), # leo agent — 8 evaluator *[ ("leo", "evaluator", 0.05, 500 + i, None, "internet-finance" if i % 2 == 0 else "ai-alignment", "now") for i in range(8) ], # cnbc org — 2 originator (legacy data, kept by classifier+gate split) ("cnbc", "originator", 0.15, 600, "domains/internet-finance/y.md", "internet-finance", "now,-5 days"), ("cnbc", "originator", 0.15, 601, "domains/internet-finance/z.md", "internet-finance", "now,-5 days"), # newhandle — handle in events but no contributors row (LEFT JOIN COALESCE → person) # -2 days so 24h-window test exclusion is unambiguous (matches alice). ("newhandle", "author", 0.30, 700, None, "ai-alignment", "now,-2 days"), ] for handle, role, weight, pr_num, claim_path, domain, ts_modifier in events: # Use SQLite datetime() to compute timestamps relative to "now" so tests # are deterministic across days. Multi-arg form: datetime('now', '-1 days'). ts_args = ts_modifier.split(",") if len(ts_args) == 1: ts_sql = f"datetime('{ts_args[0]}')" else: ts_sql = f"datetime('{ts_args[0]}', '{ts_args[1].strip()}')" conn.execute( f"""INSERT INTO contribution_events (handle, kind, role, weight, pr_number, claim_path, domain, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, {ts_sql})""", (handle, "agent" if handle in {"rio", "leo"} else "person", role, weight, pr_num, claim_path, domain), ) conn.commit() conn.close() return str(p) def _call(db_path, **query): """Build a mocked request, call handle_leaderboard, return parsed JSON.""" qs = "&".join(f"{k}={v}" for k, v in query.items()) req = make_mocked_request("GET", f"/api/leaderboard?{qs}") # make_mocked_request gives us req.app — write db_path into it. req.app["db_path"] = db_path response = asyncio.run(handle_leaderboard(req)) return json.loads(response.body.decode()) # ─── _parse_window unit tests ──────────────────────────────────────────────── class TestParseWindow: def test_default_is_all_time(self): clause, params, label = _parse_window(None) assert clause == "" assert params == () assert label == "all_time" def test_explicit_all_time(self): clause, params, label = _parse_window("all_time") assert clause == "" assert label == "all_time" def test_seven_days(self): clause, params, label = _parse_window("7d") assert clause == "ce.timestamp >= datetime('now', ?)" assert params == ("-7 days",) assert label == "7d" # Regression: must NOT begin with "AND " (handle_leaderboard composes via " AND ".join) assert not clause.startswith("AND") def test_thirty_days(self): clause, params, label = _parse_window("30d") assert params == ("-30 days",) assert label == "30d" def test_hours(self): clause, params, label = _parse_window("24h") assert clause == "ce.timestamp >= datetime('now', ?)" assert params == ("-24 hours",) assert label == "24h" def test_caps_days_at_365(self): clause, params, label = _parse_window("9999d") assert params == ("-365 days",) def test_caps_hours_at_8760(self): clause, params, label = _parse_window("99999h") assert params == ("-8760 hours",) def test_garbage_falls_to_all_time(self): clause, params, label = _parse_window("foobar") assert clause == "" assert label == "all_time" def test_uppercase_normalized(self): clause, params, label = _parse_window("7D") assert label == "7d" def test_zero_days_still_emits_clause(self): # 0d means "now or later" — empty result, but parse should succeed clause, params, label = _parse_window("0d") assert "datetime" in clause assert label == "0d" # ─── handle_leaderboard integration tests ──────────────────────────────────── class TestLeaderboardEndpoint: def test_all_time_default_kind_person(self, db_path): """Default kind is 'person'. Returns all persons, sorted by CI desc.""" body = _call(db_path) assert body["window"] == "all_time" assert body["kind_filter"] == "person" assert body["domain"] is None assert body["source"] == "contribution_events" # alice 3*0.30 + 0.15 = 1.05 # bob 5*0.30 = 1.50 # carol 0.30 + 0.05 = 0.35 # newhandle 0.30 (LEFT JOIN COALESCE → 'person') # cnbc excluded (kind='org') # rio/leo excluded (kind='agent') handles = [r["handle"] for r in body["leaderboard"]] assert "bob" in handles assert "alice" in handles assert "newhandle" in handles, "LEFT JOIN COALESCE should default missing contributors to 'person'" assert "cnbc" not in handles, "kind=person should exclude orgs" assert "rio" not in handles, "kind=person should exclude agents" # Descending by CI cis = [r["ci"] for r in body["leaderboard"]] assert cis == sorted(cis, reverse=True) def test_window_7d_excludes_old_events(self, db_path): """REGRESSION: 7d window must execute (no AND-prefix SQL error). Bob has all events 60d ago → must not appear in 7d window. Alice has events 1-3d ago → must appear. """ body = _call(db_path, window="7d") assert body["window"] == "7d" handles = [r["handle"] for r in body["leaderboard"]] assert "alice" in handles assert "bob" not in handles, "60d-old events must be excluded from 7d window" assert "carol" in handles # today def test_window_30d_excludes_60d_events(self, db_path): """REGRESSION: 30d window must execute. Bob (60d) excluded; alice/carol included.""" body = _call(db_path, window="30d") assert body["window"] == "30d" handles = [r["handle"] for r in body["leaderboard"]] assert "alice" in handles assert "carol" in handles assert "bob" not in handles def test_window_24h_only_today(self, db_path): """24h window picks up today's events only. Default kind=person. Within 24h: only carol (events at 'now'). Excluded: alice/newhandle (events at -2 days), bob (-60d), rio/leo (kind), cnbc (-5d AND kind=org). """ body = _call(db_path, window="24h") handles = [r["handle"] for r in body["leaderboard"]] assert handles == ["carol"], ( "24h + kind=person should return only carol; got %r" % handles ) def test_kind_agent(self, db_path): """kind=agent returns only agents.""" body = _call(db_path, kind="agent") handles = [r["handle"] for r in body["leaderboard"]] assert "rio" in handles assert "leo" in handles assert "alice" not in handles assert "bob" not in handles def test_kind_org(self, db_path): """kind=org returns only orgs (legacy events still queryable).""" body = _call(db_path, kind="org") handles = [r["handle"] for r in body["leaderboard"]] assert handles == ["cnbc"] assert body["leaderboard"][0]["ci"] == 0.30 # 2 * 0.15 def test_kind_all_returns_everyone(self, db_path): """kind=all returns all kinds — persons + agents + orgs.""" body = _call(db_path, kind="all") handles = {r["handle"] for r in body["leaderboard"]} assert handles == {"alice", "bob", "carol", "rio", "leo", "cnbc", "newhandle"} def test_invalid_kind_falls_to_person(self, db_path): """Defensive: unknown kind value silently falls back to 'person'.""" body = _call(db_path, kind="bogus") assert body["kind_filter"] == "person" def test_domain_filter(self, db_path): """domain=internet-finance scopes events; kind filter still applies.""" body = _call(db_path, domain="internet-finance") assert body["domain"] == "internet-finance" handles = {r["handle"] for r in body["leaderboard"]} # alice has 2 internet-finance authors + 1 originator # carol has 1 internet-finance author + 1 evaluator # bob has 0 (all ai-alignment) # newhandle has 0 (ai-alignment only) assert "alice" in handles assert "carol" in handles assert "bob" not in handles assert "newhandle" not in handles def test_composed_window_kind_domain(self, db_path): """REGRESSION: composed filters must build SQL correctly. 7d + person + internet-finance — alice only. """ body = _call(db_path, window="7d", kind="person", domain="internet-finance") handles = [r["handle"] for r in body["leaderboard"]] assert "alice" in handles assert "carol" in handles assert "bob" not in handles # excluded by 7d assert "rio" not in handles # excluded by kind=person def test_limit_caps_results(self, db_path): """limit caps the leaderboard slice; total reflects unfiltered count.""" body = _call(db_path, kind="all", limit=3) assert body["shown"] == 3 assert body["has_more"] is True assert body["total"] == 7 def test_no_has_more_when_under_limit(self, db_path): body = _call(db_path, kind="org") assert body["shown"] == 1 assert body["has_more"] is False assert body["total"] == 1 def test_invalid_limit_falls_to_default(self, db_path): """Defensive: garbage limit param falls to default 100. 7 entries < 100.""" body = _call(db_path, kind="all", limit="not-a-number") assert body["shown"] == 7 assert body["has_more"] is False def test_limit_capped_at_500(self, db_path): """Defensive: limit > 500 silently caps at 500.""" body = _call(db_path, limit=99999, kind="all") # No assertion on the value of the cap from the response — just that # it doesn't error and shown <= 500. assert body["shown"] <= 500 def test_role_breakdown_present(self, db_path): """Each row includes ci_breakdown with all 5 roles.""" body = _call(db_path) for entry in body["leaderboard"]: assert set(entry["ci_breakdown"].keys()) == { "author", "challenger", "synthesizer", "originator", "evaluator", } def test_alice_role_breakdown_correct(self, db_path): """Alice has 3 author (0.90) + 1 originator (0.15) = 1.05 total.""" body = _call(db_path) alice = next(r for r in body["leaderboard"] if r["handle"] == "alice") assert alice["ci"] == 1.05 assert alice["ci_breakdown"]["author"] == 0.90 assert alice["ci_breakdown"]["originator"] == 0.15 assert alice["ci_breakdown"]["challenger"] == 0 assert alice["ci_breakdown"]["synthesizer"] == 0 assert alice["ci_breakdown"]["evaluator"] == 0 assert alice["events_count"] == 4 assert alice["pr_count"] == 4 assert alice["domain_count"] == 2 # internet-finance + ai-alignment def test_empty_window_returns_clean_response(self, db_path): """Window with no matching events returns shape-correct empty response.""" # 24h window + kind=org → cnbc is 5d ago, so empty body = _call(db_path, window="24h", kind="org") assert body["leaderboard"] == [] assert body["total"] == 0 assert body["shown"] == 0 assert body["has_more"] is False assert body["source"] == "contribution_events" def test_left_join_handles_missing_contributors_row(self, db_path): """REGRESSION: handle in events but missing from contributors must default to kind='person'. Catches the failure mode where a handle classified as cited (auto-create deferred to Branch 3) accumulates events but has no contributors row yet. """ body = _call(db_path) newhandle_row = next( (r for r in body["leaderboard"] if r["handle"] == "newhandle"), None ) assert newhandle_row is not None assert newhandle_row["kind"] == "person" assert newhandle_row["ci"] == 0.30 # ─── Public path constant (auth middleware bypass) ─────────────────────────── def test_public_paths_includes_leaderboard(): """Auth middleware needs LEADERBOARD_PUBLIC_PATHS to skip API key for /api/leaderboard.""" assert "/api/leaderboard" in LEADERBOARD_PUBLIC_PATHS def test_kind_values_matches_contract(): """API contract: only these 4 kind values are accepted.""" assert set(KIND_VALUES) == {"person", "agent", "org", "all"}