Compare commits
No commits in common. "1351db70a9099647befebbf180b21721856dac47" and "cd5aac5cc6331ceb839f11a0d3bd21448c94d0cd" have entirely different histories.
1351db70a9
...
cd5aac5cc6
2 changed files with 0 additions and 545 deletions
|
|
@ -1,108 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Reset m3taversal.sourcer_count from inflated legacy value to file-truth count.
|
|
||||||
|
|
||||||
Background: pre-Phase-A extract.py had a `submitted_by` fallback that credited
|
|
||||||
m3taversal as sourcer for every Telegram-ingested source, accumulating to 1011
|
|
||||||
sourcer_count in the contributors table. The actual file-truth count (sourcer
|
|
||||||
frontmatter equal to "m3taversal" in claim files) is 21. The 990-row delta is
|
|
||||||
infrastructure attribution that doesn't reflect content authorship.
|
|
||||||
|
|
||||||
The Phase A event-sourced ledger (contribution_events) computed the correct
|
|
||||||
389.55 CI from author events; /api/leaderboard reads from there directly.
|
|
||||||
But the legacy /api/contributors endpoint reads contributors.claims_merged
|
|
||||||
which carries the inflated 1011. Until that endpoint is deprecated, the
|
|
||||||
divergence shows two different numbers depending on which surface the UI
|
|
||||||
queries.
|
|
||||||
|
|
||||||
This script applies the surgical UPDATE that was run on VPS on 2026-04-27
|
|
||||||
during the leaderboard cutover. Committed as a script per Ganymede review:
|
|
||||||
"DB mutations go through reviewable code paths matters more than the
|
|
||||||
convenience of one-shot SQL. The artifact explains what was done and why."
|
|
||||||
|
|
||||||
Idempotent — safe to re-run. If sourcer_count is already 21, no change.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python3 scripts/reset-m3taversal-sourcer.py --dry-run
|
|
||||||
python3 scripts/reset-m3taversal-sourcer.py
|
|
||||||
"""
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
DB_PATH = os.environ.get("PIPELINE_DB", "/opt/teleo-eval/pipeline/pipeline.db")
|
|
||||||
TARGET_HANDLE = "m3taversal"
|
|
||||||
TRUTH_SOURCER_COUNT = 21
|
|
||||||
TRUTH_CLAIMS_MERGED = 21
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--dry-run", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if not Path(DB_PATH).exists():
|
|
||||||
print(f"ERROR: DB not found at {DB_PATH}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
conn = sqlite3.connect(DB_PATH, timeout=30)
|
|
||||||
conn.row_factory = sqlite3.Row
|
|
||||||
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT handle, sourcer_count, claims_merged FROM contributors WHERE handle = ?",
|
|
||||||
(TARGET_HANDLE,),
|
|
||||||
).fetchone()
|
|
||||||
if not row:
|
|
||||||
print(f" No contributors row for {TARGET_HANDLE} — nothing to reset.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(
|
|
||||||
f" Current: {row['handle']} sourcer_count={row['sourcer_count']} "
|
|
||||||
f"claims_merged={row['claims_merged']}"
|
|
||||||
)
|
|
||||||
print(f" Target: sourcer_count={TRUTH_SOURCER_COUNT} claims_merged={TRUTH_CLAIMS_MERGED}")
|
|
||||||
|
|
||||||
if (row["sourcer_count"] == TRUTH_SOURCER_COUNT
|
|
||||||
and row["claims_merged"] == TRUTH_CLAIMS_MERGED):
|
|
||||||
print(" Already at target values — no-op.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if args.dry_run:
|
|
||||||
print(" (dry-run) UPDATE would be applied. Re-run without --dry-run.")
|
|
||||||
return
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"""UPDATE contributors SET
|
|
||||||
sourcer_count = ?,
|
|
||||||
claims_merged = ?,
|
|
||||||
updated_at = datetime('now')
|
|
||||||
WHERE handle = ?""",
|
|
||||||
(TRUTH_SOURCER_COUNT, TRUTH_CLAIMS_MERGED, TARGET_HANDLE),
|
|
||||||
)
|
|
||||||
conn.execute(
|
|
||||||
"""INSERT INTO audit_log (stage, event, detail) VALUES (?, ?, ?)""",
|
|
||||||
(
|
|
||||||
"manual",
|
|
||||||
"m3taversal_sourcer_reset",
|
|
||||||
(
|
|
||||||
'{"reason":"Pre-Phase-A submitted_by fallback inflated to 1011; '
|
|
||||||
'file-truth is 21","sourcer_count_before":1011,'
|
|
||||||
'"sourcer_count_after":21,"claims_merged_after":21}'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
after = conn.execute(
|
|
||||||
"SELECT sourcer_count, claims_merged FROM contributors WHERE handle = ?",
|
|
||||||
(TARGET_HANDLE,),
|
|
||||||
).fetchone()
|
|
||||||
print(
|
|
||||||
f" Applied. Now: sourcer_count={after['sourcer_count']} "
|
|
||||||
f"claims_merged={after['claims_merged']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,437 +0,0 @@
|
||||||
"""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"}
|
|
||||||
Loading…
Reference in a new issue