Newer
Older
veriq / scripts / test_scoring.py
#!/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())