You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
158 lines
5.4 KiB
158 lines
5.4 KiB
"""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
|
|
|