#!/usr/bin/env python3
"""VerIQ scoring unit tests. Run from repo root: python3 scripts/test_scoring.py"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from backend import scoring
failures = []
def check(name, cond, detail=""):
if cond:
print(f" PASS {name}")
else:
print(f" FAIL {name} {detail}")
failures.append(name)
def main():
bank = scoring.load_bank()
tasks = bank["tasks"]
ids = [t["id"] for t in tasks]
max_raw = scoring.max_raw_score(ids)
print(f"bank v{scoring.bank_version()}: {len(ids)} tasks, max_raw={max_raw}")
# --- raw_score ---
print("[raw_score]")
all_correct = {tid: t["answer_index"] for tid, t in zip(ids, tasks)}
check("all correct = max_raw",
scoring.raw_score(all_correct, ids) == max_raw,
f"got {scoring.raw_score(all_correct, ids)}, want {max_raw}")
all_wrong = {tid: (t["answer_index"] + 1) % len(t["choices"])
for tid, t in zip(ids, tasks)}
check("all wrong = 0", scoring.raw_score(all_wrong, ids) == 0)
half = {tid: t["answer_index"] if i % 2 == 0 else (t["answer_index"] + 1) % len(t["choices"])
for i, (tid, t) in enumerate(zip(ids, tasks))}
expected_half = sum(t["weight"] for i, t in enumerate(tasks) if i % 2 == 0)
check("half correct = sum of even-index weights",
scoring.raw_score(half, ids) == expected_half,
f"got {scoring.raw_score(half, ids)}, want {expected_half}")
# unknown task ids are ignored, missing = wrong
partial = {ids[0]: tasks[0]["answer_index"]}
check("unknown ids ignored",
scoring.raw_score({**partial, "nope-999": 0}, ids) == tasks[0]["weight"])
# --- sanity_check (bot flags) ---
print("[sanity_check]")
check("too fast + perfect = flagged",
scoring.sanity_check(all_correct, ids, 60.0) is True,
"40 tasks in 60s = 1.5s/task, perfect score")
check("too fast at all = flagged",
scoring.sanity_check(all_wrong, ids, 30.0) is True)
check("plausible pace = not flagged",
scoring.sanity_check(all_wrong, ids, 1200.0) is False,
"30s/task, wrong answers")
check("plausible pace + perfect = not flagged",
scoring.sanity_check(all_correct, ids, 1800.0) is False,
"45s/task, perfect")
# --- score_test: IQ bounds and percentile ---
print("[score_test]")
r = scoring.score_test(all_correct, ids, 1800.0, False)
check("perfect -> IQ >= 130 (prior model)",
r["iq"] >= 130, f"got {r['iq']}")
check("perfect -> percentile > 99", r["percentile"] > 99, f"got {r['percentile']}")
check("not calibrated yet", r["calibrated"] is False)
r2 = scoring.score_test(all_wrong, ids, 1200.0, False)
check("all wrong -> IQ <= 70", r2["iq"] <= 70, f"got {r2['iq']}")
check("all wrong -> percentile < 1", r2["percentile"] < 1, f"got {r2['percentile']}")
r3 = scoring.score_test(half, ids, 1500.0, False)
check("half correct -> IQ in 80..120 band",
80 <= r3["iq"] <= 120, f"got {r3['iq']}")
check("categories present", set(r3["categories"].keys()) ==
{"numeric", "matrix", "verbal", "spatial"})
# --- calibration fallback ---
print("[calibration]")
check("calibration file exists",
scoring.CALIB_PATH.exists())
import json
cal = json.loads(scoring.CALIB_PATH.read_text())
check("calibration n=0 -> prior used", cal.get("n", 0) < scoring.MIN_CALIBRATION_N)
print()
if failures:
print(f"FAILED: {len(failures)} -> {failures}")
return 1
print("ALL SCORING TESTS PASSED")
return 0
if __name__ == "__main__":
sys.exit(main())