diff --git a/frontend/assets/result.js b/frontend/assets/result.js index 49319df..e8d85fc 100644 --- a/frontend/assets/result.js +++ b/frontend/assets/result.js @@ -84,6 +84,40 @@ setTimeout(function () { $("copyBtn").textContent = "Copy"; }, 1500); }); + // claim buttons: ask backend for authorize_url, redirect; 503 -> hint + function wireClaim(btnId, provider) { + var btn = $(btnId); + if (!btn) return; + btn.addEventListener("click", function () { + btn.disabled = true; + fetch("/auth/" + provider + "/start?result_id=" + rid) + .then(function (r) { + if (r.status === 503) { + throw new Error("not_configured"); + } + if (!r.ok) throw new Error("http " + r.status); + return r.json(); + }) + .then(function (data) { + window.location.href = data.authorize_url; + }) + .catch(function (e) { + btn.disabled = false; + var err = $("claimErr"); + err.style.display = ""; + if (e.message === "not_configured") { + err.textContent = "Identity binding is being set up — " + + "the service owner must register the X/Instagram apps first. " + + "Your result is already signed and verifiable without it."; + } else { + err.textContent = "Claim failed: " + e.message; + } + }); + }); + } + wireClaim("claimX", "x"); + wireClaim("claimIg", "ig"); + var msg = "My VerIQ score: " + d.iq + " (higher than " + pct + "%). Verify it: " + link; $("shareX").href = "https://twitter.com/intent/tweet?text=" + diff --git a/frontend/result.html b/frontend/result.html index 3d03022..1ab2834 100644 --- a/frontend/result.html +++ b/frontend/result.html @@ -41,11 +41,17 @@
-

Claim this result (coming next)

-

- Soon you'll be able to bind this result to your X or Instagram account — - the verify page will then show your handle and avatar next to the score. +

Claim this result

+

+ Bind this result to your X or Instagram account — the verify page will + then show your handle next to the score. Nobody will be able to steal + your result by copying the link.

+
+ + +
+
diff --git a/scripts/test_scoring.py b/scripts/test_scoring.py new file mode 100644 index 0000000..d5d6aeb --- /dev/null +++ b/scripts/test_scoring.py @@ -0,0 +1,100 @@ +#!/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())