"""Tests for build_mp_quiz_tab and related quiz DB integration. Task 2.1 + Task 3.1: smoke test that the builder is exported and callable, plus an end-to-end simulation of the quiz matching logic via real temp DuckDB. """ import importlib from pathlib import Path import duckdb from database import MotionDatabase # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _create_motion(db: MotionDatabase, url: str, title: str, controversy: float = 0.5): md = { "title": title, "description": title, "date": "2023-01-01", "policy_area": "test", "voting_results": {}, "winning_margin": controversy, "layman_explanation": f"Uitleg over {title}", "url": url, } ok = db.insert_motion(md) assert ok, f"insert_motion failed for {url}" conn = duckdb.connect(db.db_path) row = conn.execute("SELECT id FROM motions WHERE url = ?", (url,)).fetchone() conn.close() assert row is not None return int(row[0]) # --------------------------------------------------------------------------- # Task 2.1 — smoke test: builder exported and callable # --------------------------------------------------------------------------- def test_builder_exists(): """build_mp_quiz_tab must be importable from explorer and callable.""" mod = importlib.import_module("explorer") assert hasattr(mod, "build_mp_quiz_tab"), "build_mp_quiz_tab not found in explorer" assert callable(mod.build_mp_quiz_tab) # --------------------------------------------------------------------------- # Task 3.1 — end-to-end quiz matching simulation # --------------------------------------------------------------------------- def test_quiz_unique_match(tmp_path: Path): """Full quiz flow: 6 motions, 4 MPs — pre-filled votes produce a unique match.""" db_path = str(tmp_path / "quiz_e2e.db") db = MotionDatabase(db_path) # 6 motions mids = [_create_motion(db, f"http://qe{i}", f"Motion {i}") for i in range(6)] # 4 MPs with distinct voting patterns mpA = "Alpha, A." mpB = "Beta, B." mpC = "Gamma, G." mpD = "Delta, D." patterns = { mpA: ["voor", "voor", "voor", "voor", "voor", "voor"], # all voor mpB: ["tegen", "tegen", "voor", "voor", "voor", "voor"], # 2 tegen then 4 voor mpC: ["voor", "tegen", "voor", "tegen", "voor", "tegen"], # alternating mpD: ["tegen", "tegen", "tegen", "tegen", "tegen", "voor"], # mostly tegen } for idx, mid in enumerate(mids): for mp, votes in patterns.items(): db.insert_mp_vote(mid, mp, votes[idx], "2023-01-01") # User votes matching mpA exactly (all Voor) user_votes = {mid: "Voor" for mid in mids} results = db.match_mps_for_votes(user_votes, limit=50) assert results, "Expected ranked results" top = results[0] assert top["mp_name"] == mpA assert top["agreement_pct"] == 100.0 # Unique match: only mpA should be at 100% top_matches = [r for r in results if r["agreement_pct"] == top["agreement_pct"]] assert len(top_matches) == 1, f"Expected unique top match, got {top_matches}" def test_quiz_indistinguishable_mps(tmp_path: Path): """When two MPs vote identically, both appear at the top with same agreement_pct.""" db_path = str(tmp_path / "quiz_indist.db") db = MotionDatabase(db_path) mids = [ _create_motion(db, f"http://ind{i}", f"Indist Motion {i}") for i in range(3) ] mpA = "Alice, A." mpB = "Bob, B." # Both vote identically for mid in mids: db.insert_mp_vote(mid, mpA, "voor", "2023-01-01") db.insert_mp_vote(mid, mpB, "voor", "2023-01-01") user_votes = {mid: "Voor" for mid in mids} results = db.match_mps_for_votes(user_votes, limit=50) assert len(results) == 2, "Both identical MPs should appear" assert results[0]["agreement_pct"] == results[1]["agreement_pct"] == 100.0 def test_quiz_discriminating_reduces_candidates(tmp_path: Path): """After one discriminating question, candidate set should shrink.""" db_path = str(tmp_path / "quiz_disc.db") db = MotionDatabase(db_path) # 4 motions: motion0 splits mpA from mpB/mpC mids = [_create_motion(db, f"http://disc{i}", f"Disc Motion {i}") for i in range(4)] mpA = "Alpha, A." mpB = "Beta, B." mpC = "Gamma, G." # motion0 splits; motions 1-3 they all agree db.insert_mp_vote(mids[0], mpA, "voor", "2023-01-01") db.insert_mp_vote(mids[0], mpB, "tegen", "2023-01-01") db.insert_mp_vote(mids[0], mpC, "tegen", "2023-01-01") for i in range(1, 4): for mp in (mpA, mpB, mpC): db.insert_mp_vote(mids[i], mp, "voor", "2023-01-01") # All three agree on motions 1-3; choose_discriminating_motions should pick motion0 all_mps = [mpA, mpB, mpC] chosen = db.choose_discriminating_motions(all_mps, excluded_motion_ids=[], k=1) assert chosen[0] == mids[0], ( f"Expected motion {mids[0]} as discriminating, got {chosen}" ) # After user answers Voor on motion0, only mpA should rank at 100% user_votes = {mids[0]: "Voor"} results = db.match_mps_for_votes(user_votes, limit=50) top = results[0] assert top["mp_name"] == mpA assert top["agreement_pct"] == 100.0 # mpB and mpC should be at 0% for r in results: if r["mp_name"] in (mpB, mpC): assert r["agreement_pct"] == 0.0