"""Unit tests for scraper.bill_id -- the canonical bill_id codec (spec section 4). These tests are the source of truth for the codec the parser depends on, so they run first and are the most brittle-to-change. Any change to the codec that breaks any of these must be a conscious decision, not an accidental refactor. """ from __future__ import annotations import pytest from scraper.bill_id import ( normalise_session, origin_house, parse_bill_id, parse_display, parse_supplement, ) # ---------------------------------------------------------------- parse_bill_id class TestParseBillId: def test_assembly_bill_ab28(self): dec = parse_bill_id("202520260AB28") assert dec is not None assert dec["session"] == "2025-2026" assert dec["session_raw"] == "20252026" assert dec["chamber"] == "AB" assert dec["number"] == 28 assert dec["display"] == "AB 28" assert dec["house"] == "assembly" assert dec["raw"] == "202520260AB28" def test_senate_bill_sb795(self): dec = parse_bill_id("202520260SB795") assert dec is not None assert dec["chamber"] == "SB" assert dec["session"] == "2025-2026" assert dec["number"] == 795 assert dec["display"] == "SB 795" assert dec["house"] == "senate" def test_assembly_prefixes(self): for raw, chamber, house in [ ("202520260ACR5", "ACR", "assembly"), ("202520260HR7", "HR", "assembly"), ("202520260ABR1", "ABR", "assembly"), ]: dec = parse_bill_id(raw) assert dec is not None assert dec["chamber"] == chamber, raw assert dec["house"] == house, raw def test_senate_prefixes(self): for raw, chamber in [ ("202520260SCR12", "SCR"), ("202520260SJR3", "SJR"), ]: dec = parse_bill_id(raw) assert dec is not None assert dec["chamber"] == chamber, raw assert dec["house"] == "senate", raw def test_invalid_inputs_return_none(self): for bad in [ "", "garbage", "202520260", # no chamber/number tail "202520260AB", # chamber with no number "202520260AB28JUNK",# trailing junk "20250AB28", # wrong session length "202520260XY12", # unknown chamber prefix ]: assert parse_bill_id(bad) is None, f"expected None for {bad!r}" def test_whitespace_is_stripped(self): dec = parse_bill_id(" 202520260AB28 ") assert dec is not None assert dec["display"] == "AB 28" assert dec["raw"] == " 202520260AB28 " def test_session_decimal_boundary(self): dec = parse_bill_id("202520260AB40") assert dec is not None assert dec["number"] == 40 assert dec["display"] == "AB 40" # ---------------------------------------------------------------- normalise_session class TestNormaliseSession: def test_standard_session(self): assert normalise_session("20252026") == "2025-2026" def test_empty(self): assert normalise_session("") == "" def test_non_session_passthrough(self): assert normalise_session("garbage") == "garbage" def test_four_digit_passthrough(self): assert normalise_session("2025") == "2025" # ---------------------------------------------------------------- origin_house class TestOriginHouse: def test_assembly_prefixes(self): for chamber in ("AB", "ACR", "HR", "ABR"): assert origin_house(chamber) == "assembly", chamber def test_senate_prefixes(self): for chamber in ("SB", "SCR", "SJR"): assert origin_house(chamber) == "senate", chamber def test_unknown(self): assert origin_house("ZZ") == "unknown" assert origin_house("") == "unknown" # ---------------------------------------------------------------- parse_display class TestParseDisplay: def test_padded(self): assert parse_display("AB 00028") == ("AB", 28) def test_unpadded(self): assert parse_display("AB 28") == ("AB", 28) def test_various_prefixes(self): assert parse_display("SB 795") == ("SB", 795) assert parse_display("ACR 5") == ("ACR", 5) assert parse_display("SCR 12") == ("SCR", 12) def test_invalid(self): for garbage in ("", "SB", "AB 7 X", "XYZ 5", " "): assert parse_display(garbage) is None, garbage # ---------------------------------------------------------------- parse_supplement class TestParseSupplement: def test_standard(self): assert parse_supplement("Supplemental 2") == 2 def test_case_insensitive(self): assert parse_supplement("supplemental 1") == 1 def test_no_match(self): for text in ("", "plain text", "Supplemental", "Supp 3"): assert parse_supplement(text) is None, text