Initial commit

This commit is contained in:
kfj001 committed 2026-09-06 10:18:26 -07:00
commit 5f2db08b43
100 files changed
+16985

No files matched your search

+216
View File
@@ -0,0 +1,216 @@
"""Tests for the dashboard ranking / float-to-top logic (spec §5, DESIGN §4).
Pure-logic coverage of the heart of §5, no DB:
* a WATCHED-LEGISLATOR match raises a bill's score so it floats to the top;
* a TAG match (opted-in topic) raises a bill's score so it floats to the top;
* ANON view has NO relevance and NO personalized sort (personal=off excludes it);
* hearing_sort orders "Upon Call of the Chair" last, times asc;
* a T-1 day is excluded from an upcoming view;
* "Why is this here?" (matched[]) transparency carries the trigger.
"""
from __future__ import annotations
from scraper.dashboard import ranking
from scraper.dashboard.ranking import (
hearing_sort, score_bill, normalise_bill, float_to_top,
personal_band, full_queue, is_floated, Weights,
)
from scraper.dashboard.context import RelevanceContext, from_prefs
def _bill(bill_id, author, committee=None, tags=None, chamber="ASSEMBLY",
hearing_time="9:30am", date="2026-09-07",
short_title="t", description="d"):
b = {
"bill_id": bill_id,
"bill_number": bill_id,
"chamber_origin": chamber,
"author": author,
"short_title": short_title,
"description": description,
"committee": committee,
"hearing_time": hearing_time,
"location": "Room 1",
"date": date,
"session": "2025-2026",
"item_number": 1,
"supplement": 1,
"source": "ASSEMBLY_DAILY_FILE",
"tags": tags,
}
b["hearing_sort"] = hearing_sort(hearing_time)
return b
def _bills():
# Three bills, all on the same day. B2 is authored by a watched legislator;
# B3 is in a topic (education) the user opted into; B1 matches nothing.
return [
_bill("A1", "Unrelated McPerson", committee="General",
tags=["budget"], date="2026-09-07"),
_bill("A2", "Schiavo et al.", committee="Resources and Water",
date="2026-09-07"), # author watched path
_bill("A3", "Macedo", committee=None, tags=["education", "health"],
date="2026-09-07"), # tag match path
]
# ---------------------------------------------------------------------------
# hearing_sort
# ---------------------------------------------------------------------------
def test_hearing_sort_walltime_ascending():
assert hearing_sort("9:30am") == 9 * 3600 + 30 * 60
assert hearing_sort("10:00am") > hearing_sort("9:30am")
assert hearing_sort("1:00pm") > hearing_sort("12:00pm") # 1pm = 13:00
assert hearing_sort("12:00am") == 0 # midnight
def test_hearing_sort_call_of_chair_and_null_sort_last():
last = hearing_sort("Upon Call of the Chair")
assert last == hearing_sort("Call of the Chair")
assert last == hearing_sort(None) # missing time sinks (time TBD)
assert last == hearing_sort("")
# a real wall time sorts before call-of-the-chair / nulls.
assert hearing_sort("9:30am") < last
# ---------------------------------------------------------------------------
# score_bill — the OR rule (author OR tag suffices to float)
# ---------------------------------------------------------------------------
def test_watched_author_floats():
b = _bills()[1] # authored by "Schiavo et al."
rel = score_bill(b, watched_authors={"schiavo"},
watched_committees=set(), opted_in_tags=set())
assert rel["score"] >= 3, rel
assert is_floated({**b, "relevance": rel})
# transparency: matched[] names the rule (a "Why is this here?" trigger)
types = [m["type"] for m in rel["matched"]]
assert "watched-legislator" in types
def test_opted_in_tag_floats():
# A bill that is NOT authored by a watched leg but carries an opted-in
# topic floats on the TAG path alone.
b = _bills()[2] # tags ["education","health"]
rel = score_bill(b, watched_authors=set(),
watched_committees=set(),
opted_in_tags={"education"})
assert rel["score"] > 0, rel
assert is_floated({**b, "relevance": rel})
types = [m["type"] for m in rel["matched"]]
assert "tag" in types
# matched carries the actual tag that triggered it
assert any(m["value"].lower().startswith("topic")
for m in rel["matched"])
def test_no_match_does_not_float():
# A bill matching nothing scores 0 and does NOT float.
b = _bills()[0]
rel = score_bill(b, watched_authors=set(),
watched_committees=set(), opted_in_tags=set())
assert rel["score"] == 0
assert not is_floated({**b, "relevance": rel})
def test_both_paths_stack_score():
# Author watched + an opted-in tag both present -> score > either alone.
b = _bill("A2", "Schiavo et al.", tags=["education"])
rel = score_bill(b, watched_authors={"schiavo"},
watched_committees=set(), opted_in_tags={"education"})
alone = score_bill(b, watched_authors={"schiavo"},
watched_committees=set(), opted_in_tags=set())
assert rel["score"] > alone["score"]
assert {"watched-legislator", "tag"}.issubset(
{m["type"] for m in rel["matched"]})
def test_committee_of_watched_legislator_path():
# A bill in a committee a watched legislator chairs/serves on floats even
# with no author/tag match (committee->members join supplied).
b = _bill("A4", "Otherperson", committee="Emergency Management",
tags=[], )
rel = score_bill(b, watched_authors=set(),
watched_committees={"emergency-management"},
opted_in_tags=set())
assert rel["score"] > 0, rel
assert "committee-of-watched-legislator" in [
m["type"] for m in rel["matched"]]
# ---------------------------------------------------------------------------
# float-to-top order + anon exclusion of personalized sort
# ---------------------------------------------------------------------------
def _personal_bills():
bills = _bills()
ctx = from_prefs({"watched_legislators": ["schiavo"],
"opted_in_tags": ["education"]})
for b in bills:
b["relevance"] = ctx.score(b)
return bills, ctx
def test_float_to_top_puts_relevant_first():
bills, ctx = _personal_bills()
assert ctx.is_personalised
ordered = float_to_top(bills)
# A2 (author) and A3 (tag) float ahead of A1 (no match).
ids = [b["bill_id"] for b in ordered]
assert ids.index("A2") < ids.index("A1")
assert ids.index("A3") < ids.index("A1")
# floated band = top N of the floated bills
band = personal_band(bills)
assert {b["bill_id"] for b in band} == {"A2", "A3"}
assert all(is_floated(b) for b in band)
def test_personal_band_bounded_to_n():
bills, ctx = _personal_bills()
band = personal_band(bills, n=1)
assert len(band) == 1
# higher score first (A2 = author+nothing vs A3 = tag+nothing both score
# 3 and 2 respectively; A2 should lead when scores differ)
a2 = next(b for b in bills if b["bill_id"] == "A2")
a3 = next(b for b in bills if b["bill_id"] == "A3")
assert a2["relevance"]["score"] >= a3["relevance"]["score"]
assert band[0]["bill_id"] == "A2"
def test_full_queue_promotes_but_hides_nothing():
# Every bill remains present; only order changes (promote, not hide).
bills, ctx = _personal_bills()
ordered = full_queue(bills)
assert {b["bill_id"] for b in ordered} == {"A1", "A2", "A3"}
assert len(ordered) == 3
def test_anon_view_has_no_relevance_and_no_personal_sort():
# The anon envelope carries NO relevance block and is NOT float-sorted:
# the personal=off path must not reorder anything.
bills = _bills()
anon = [dict(b, **{"relevance": None}) for b in bills]
# strip relevance to mirror the anon projection
anon = [{k: v for k, v in b.items() if k != "relevance"} for b in _bills()]
assert all("relevance" not in b for b in anon)
# anon ordering == stable input order (no float applied)
ordered = float_to_top([
{k: v for k, v in b.items() if k != "relevance"} for b in _bills()])
assert [b["bill_id"] for b in ordered] == ["A1", "A2", "A3"]
def test_weights_are_tunable():
# D3: weights are knobs. Bumping the tag weight changes band ordering.
bills, ctx = _personal_bills()
hi = Weights(tag_watched=10)
for b in bills:
b["relevance"] = score_bill(b,
watched_authors=ctx.watched_authors,
watched_committees=ctx.watched_committees,
opted_in_tags=ctx.opted_in_tags, weights=hi)
ordered = float_to_top(bills)
# A3 (tag, weight 10) now outranks A2 (author, weight 3)
assert ordered[0]["bill_id"] == "A3"