● Zion Boggan
repos/Docket/services/ingest/tests/test_ranking.py
zionboggan.com ↗
141 lines · python
History for this file →
1
"""
2
Tests for ingest.ranking  -  neutrality rule
3
 
4
Key assertion: the ranking function references ONLY the four permitted fields
5
(actionDate, dollarAmount, affectedEstimate, actionType) and no
6
engagement/news/social inputs.
7
"""
8
 
9
import ast
10
import inspect
11
import textwrap
12
 
13
import pytest
14
 
15
from ingest.ranking import (
16
    FORBIDDEN_RANKING_FIELDS,
17
    assert_no_forbidden_fields,
18
    score,
19
    _recency_score,
20
    _dollar_score,
21
    _affected_score,
22
    _type_score,
23
)
24
 
25
 
26
 
27
class TestScoreRange:
28
    def test_score_in_unit_interval(self):
29
        s = score("2026-06-25", 1_000_000, 50_000, "signed")
30
        assert 0.0 <= s <= 1.0
31
 
32
    def test_zero_dollar_zero_affected(self):
33
        s = score("2026-06-25", None, None, "floor_vote")
34
        assert 0.0 <= s <= 1.0
35
 
36
    def test_older_action_scores_lower(self):
37
        recent = score("2026-06-25", 1_000_000, 0, "contract")
38
        old    = score("2020-01-01", 1_000_000, 0, "contract")
39
        assert recent > old, "More recent actions should score higher"
40
 
41
    def test_higher_dollar_scores_higher(self):
42
        small = score("2026-06-25", 1_000, None, "contract")
43
        large = score("2026-06-25", 1_000_000_000, None, "contract")
44
        assert large > small
45
 
46
    def test_more_affected_scores_higher(self):
47
        few  = score("2026-06-25", None, 100, "introduced")
48
        many = score("2026-06-25", None, 10_000_000, "introduced")
49
        assert many > few
50
 
51
    def test_high_weight_action_type(self):
52
        signed    = score("2026-06-25", None, None, "signed")
53
        introduced = score("2026-06-25", None, None, "introduced")
54
        assert signed > introduced
55
 
56
    def test_unknown_action_type_does_not_crash(self):
57
        s = score("2026-06-25", 500_000, None, "UNKNOWN_TYPE_XYZ")
58
        assert 0.0 <= s <= 1.0
59
 
60
    def test_bad_date_returns_low_score(self):
61
        s = score("not-a-date", 1_000_000, 100_000, "signed")
62
        assert 0.0 <= s <= 1.0
63
 
64
    def test_returns_float(self):
65
        assert isinstance(score("2026-06-25", 1_000_000, 50_000, "grant"), float)
66
 
67
 
68
 
69
class TestNoForbiddenFields:
70
    """
71
    Neutrality rule
72
    The ranking helpers must NOT reference any engagement/news/social field.
73
    """
74
 
75
    def test_assert_no_forbidden_fields_passes(self):
76
        """assert_no_forbidden_fields() must not raise on the current module."""
77
        assert_no_forbidden_fields()
78
 
79
    def _collect_names_from_source(self, fn) -> set:
80
        src = inspect.getsource(fn)
81
        tree = ast.parse(src)
82
        names = set()
83
        for node in ast.walk(tree):
84
            if isinstance(node, ast.Name):
85
                names.add(node.id)
86
            elif isinstance(node, ast.Attribute):
87
                names.add(node.attr)
88
        return names
89
 
90
    def test_score_function_uses_no_forbidden_names(self):
91
        names = self._collect_names_from_source(score)
92
        forbidden_found = names & FORBIDDEN_RANKING_FIELDS
93
        assert not forbidden_found, (
94
            f"score() references forbidden fields: {forbidden_found}"
95
        )
96
 
97
    def test_recency_score_uses_no_forbidden_names(self):
98
        names = self._collect_names_from_source(_recency_score)
99
        forbidden_found = names & FORBIDDEN_RANKING_FIELDS
100
        assert not forbidden_found, (
101
            f"_recency_score() references forbidden fields: {forbidden_found}"
102
        )
103
 
104
    def test_dollar_score_uses_no_forbidden_names(self):
105
        names = self._collect_names_from_source(_dollar_score)
106
        forbidden_found = names & FORBIDDEN_RANKING_FIELDS
107
        assert not forbidden_found, (
108
            f"_dollar_score() references forbidden fields: {forbidden_found}"
109
        )
110
 
111
    def test_affected_score_uses_no_forbidden_names(self):
112
        names = self._collect_names_from_source(_affected_score)
113
        forbidden_found = names & FORBIDDEN_RANKING_FIELDS
114
        assert not forbidden_found, (
115
            f"_affected_score() references forbidden fields: {forbidden_found}"
116
        )
117
 
118
    def test_type_score_uses_no_forbidden_names(self):
119
        names = self._collect_names_from_source(_type_score)
120
        forbidden_found = names & FORBIDDEN_RANKING_FIELDS
121
        assert not forbidden_found, (
122
            f"_type_score() references forbidden fields: {forbidden_found}"
123
        )
124
 
125
    def test_forbidden_field_would_be_caught(self):
126
        """
127
        Sanity-check: verify that the detection logic catches a fake function
128
        that DOES reference a forbidden field.
129
        """
130
        def _bad_ranking(actionDate, dollarAmount, affectedEstimate, actionType, engagement):
131
            return engagement * dollarAmount
132
 
133
        src = textwrap.dedent(inspect.getsource(_bad_ranking))
134
        tree = ast.parse(src)
135
        names = set()
136
        for node in ast.walk(tree):
137
            if isinstance(node, ast.Name):
138
                names.add(node.id)
139
        assert "engagement" in names & FORBIDDEN_RANKING_FIELDS, (
140
            "Detection logic failed to find 'engagement' in bad function"
141
        )