@@ -996,6 +996,64 @@ def test_locate_site_class_cache_is_bounded(ag3_sim_api: AnophelesSnpData):
996996 assert len (ag3_sim_api ._cache_locate_site_class ) <= _LOCATE_SITE_CLASS_CACHE_MAXSIZE
997997
998998
999+ def test_locate_site_class_cache_lru_eviction (ag3_sim_api : AnophelesSnpData ):
1000+ """Verify true LRU semantics: recently *accessed* entries survive eviction,
1001+ while least-recently-used entries are evicted first."""
1002+ from collections import OrderedDict
1003+
1004+ from malariagen_data .anoph .snp_data import _LOCATE_SITE_CLASS_CACHE_MAXSIZE
1005+
1006+ cache = ag3_sim_api ._cache_locate_site_class
1007+
1008+ # Start from a clean cache.
1009+ cache .clear ()
1010+ assert isinstance (cache , OrderedDict )
1011+
1012+ maxsize = _LOCATE_SITE_CLASS_CACHE_MAXSIZE # 64
1013+
1014+ # --- Phase 1: fill the cache to exactly maxsize ---
1015+ dummy = np .array ([True , False ])
1016+ for i in range (maxsize ):
1017+ key = (f"contig_{ i } " , f"mask_{ i } " , f"class_{ i } " )
1018+ cache [key ] = dummy
1019+ assert len (cache ) == maxsize
1020+
1021+ # Remember the first key inserted (the oldest / least-recently-used).
1022+ first_key = ("contig_0" , "mask_0" , "class_0" )
1023+ assert first_key in cache
1024+
1025+ # --- Phase 2: simulate an access (LRU promotion) on the first key ---
1026+ # move_to_end makes it the most-recently-used entry.
1027+ cache .move_to_end (first_key )
1028+
1029+ # Insert one more entry, exceeding maxsize.
1030+ overflow_key = ("overflow" , "mask" , "class" )
1031+ cache [overflow_key ] = dummy
1032+
1033+ # Evict to maintain the bound (same logic as _locate_site_class).
1034+ while len (cache ) > maxsize :
1035+ oldest = next (iter (cache ))
1036+ del cache [oldest ]
1037+
1038+ # The first key should STILL be present because it was promoted.
1039+ assert (
1040+ first_key in cache
1041+ ), "LRU promotion via move_to_end must keep recently accessed entries alive"
1042+
1043+ # The second key ("contig_1", ...) — which was never re-accessed —
1044+ # should have been evicted as the new least-recently-used entry.
1045+ second_key = ("contig_1" , "mask_1" , "class_1" )
1046+ assert (
1047+ second_key not in cache
1048+ ), "The least-recently-used entry should be evicted when cache exceeds maxsize"
1049+
1050+ # The overflow key should be present (it was just inserted).
1051+ assert overflow_key in cache
1052+
1053+ # Cache size must remain bounded.
1054+ assert len (cache ) == maxsize
1055+
1056+
9991057def test_snp_calls_cache_is_per_instance (ag3_sim_api : AnophelesSnpData ):
10001058 """_cached_snp_calls must be a per-instance lru_cache, not a class-level one.
10011059
0 commit comments