@@ -78,3 +78,58 @@ def test_question_expire():
7878
7979 # Verify the question not longer suppressed since the cache has expired
8080 assert not history .suppresses (question , now , other_known_answers )
81+
82+
83+ def test_question_history_bounded ():
84+ """History keeps a hard cap so a LAN flood cannot grow it without bound."""
85+ history = QuestionHistory ()
86+ now = r .current_time_millis ()
87+ answers : set [r .DNSRecord ] = set ()
88+
89+ cap = const ._MAX_QUESTION_HISTORY_ENTRIES
90+ for i in range (cap + 500 ):
91+ q = r .DNSQuestion (f"_svc{ i } ._tcp.local." , const ._TYPE_PTR , const ._CLASS_IN )
92+ history .add_question_at_time (q , now , answers )
93+
94+ assert len (history ._history ) <= cap
95+
96+
97+ def test_question_history_evicts_oldest_first ():
98+ """When at cap, the oldest insertion is dropped first."""
99+ history = QuestionHistory ()
100+ now = r .current_time_millis ()
101+ answers : set [r .DNSRecord ] = set ()
102+
103+ cap = const ._MAX_QUESTION_HISTORY_ENTRIES
104+ first = r .DNSQuestion ("_first._tcp.local." , const ._TYPE_PTR , const ._CLASS_IN )
105+ history .add_question_at_time (first , now , answers )
106+
107+ # Add `cap` more fresh, non-expired entries — one past the cap — so the
108+ # final insertion forces oldest-first eviction of `first`.
109+ for i in range (cap ):
110+ q = r .DNSQuestion (f"_svc{ i } ._tcp.local." , const ._TYPE_PTR , const ._CLASS_IN )
111+ history .add_question_at_time (q , now , answers )
112+
113+ assert first not in history ._history
114+ assert len (history ._history ) <= cap
115+
116+
117+ def test_question_history_opportunistic_expire ():
118+ """Adding past the cap first drops expired entries before evicting fresh ones."""
119+ history = QuestionHistory ()
120+ old = r .current_time_millis ()
121+ answers : set [r .DNSRecord ] = set ()
122+
123+ cap = const ._MAX_QUESTION_HISTORY_ENTRIES
124+ for i in range (cap ):
125+ q = r .DNSQuestion (f"_stale{ i } ._tcp.local." , const ._TYPE_PTR , const ._CLASS_IN )
126+ history .add_question_at_time (q , old , answers )
127+
128+ # All prior entries are now stale (>999ms old). Adding one more should
129+ # trigger opportunistic expiry rather than evicting only the oldest one.
130+ fresh_now = old + const ._DUPLICATE_QUESTION_INTERVAL + 1
131+ fresh = r .DNSQuestion ("_fresh._tcp.local." , const ._TYPE_PTR , const ._CLASS_IN )
132+ history .add_question_at_time (fresh , fresh_now , answers )
133+
134+ assert fresh in history ._history
135+ assert len (history ._history ) == 1
0 commit comments