@@ -40,15 +40,29 @@ def _add(store, emb, wid, rid, text, **kw):
4040class _RecordingIndex :
4141 """Vector-index double that records every arm size it was queried with."""
4242
43- def __init__ (self ):
43+ def __init__ (self , hit_count : int | None = None , real_ids : list [str ] | None = None ):
44+ """If ``hit_count`` is set, the index always returns exactly that many
45+ hits per call (up to ``k``). ``None`` (default) returns the synthetic
46+ up-to-4 series; ``0`` returns nothing; any positive int returns that
47+ many. ``real_ids`` (if given) replaces the synthetic id prefix so the
48+ returned ids resolve to real ``MemoryRecord`` rows in the store --
49+ otherwise the prompt-eligibility filter discards them and the
50+ escalation loop is short-circuited on empty recs.
51+ """
4452 self .requested : list [int ] = []
4553 self .records : list [tuple [str , float ]] = []
54+ self ._hit_count = hit_count
55+ self ._real_ids = list (real_ids ) if real_ids else None
4656
4757 def search (self , query , k , * , filter = None ):
4858 self .requested .append (int (k ))
49- # Return synthetic (id, score) pairs so the prompt-eligible path has
50- # something to score. Use distinct ids so the loop tests candidate count.
51- return [(f"mem_{ i } " , float (k - i )) for i in range (min (k , 4 ))]
59+ if self ._hit_count == 0 :
60+ return []
61+ cap = min (k , 4 ) if self ._hit_count is None else min (k , self ._hit_count )
62+ if self ._real_ids is not None :
63+ return [(self ._real_ids [i % len (self ._real_ids )], float (k - i ))
64+ for i in range (cap )]
65+ return [(f"mem_{ i } " , float (k - i )) for i in range (cap )]
5266
5367
5468def test_arm_candidate_k_cap_default_is_none (monkeypatch ):
@@ -106,22 +120,58 @@ def test_arm_candidate_k_cap_clamps_ceiling_when_first_page_insufficient(monkeyp
106120 """The second page must also be clamped so the escalation loop does not
107121 silently undo the savings by jumping to PROMPT_ONLY_MIN_CANDIDATES."""
108122 monkeypatch .setenv ("ENGRAPHIS_RECALL_ARM_CANDIDATE_K" , "8" )
109- index = _RecordingIndex ()
123+ # Build an engine whose only enabled arm is the vector arm, so the
124+ # lexical/graph/code arms cannot pad the prompt-eligible record set
125+ # and short-circuit the ceiling path. The recording index returns
126+ # exactly 1 hit per call (less than the prompt_target of 2 below and
127+ # less than arm_candidate_k=4 so can_expand is True), so the
128+ # escalation loop is forced into the arm_candidate_k =
129+ # candidate_ceiling branch.
130+ from engraphis .core .retrieval_policy import ProfileConfig
131+ vector_only = ProfileConfig (
132+ name = "vector-only-test" , vector = True , lexical = False , graph = False , code = False
133+ )
110134 eng = RecallEngine (Store (":memory:" ), _SemanticTestEmbedder (256 ),
111- index , IdentityReranker ())
135+ _RecordingIndex () , IdentityReranker ())
112136 store = eng .store
113137 wid = store .get_or_create_workspace ("w" )
138+ real_ids = []
114139 for i in range (20 ):
115- _add (store , eng .embedder , wid , None , f"fact { i } " )
116-
117- eng .recall ("fact 0" , SearchFilter (workspace_id = wid ), k = 1 ,
118- candidate_k = 1 , prompt_only = True )
119-
120- # First arm is 1 + min(250, 1*3) = 4, ceiling would normally escalate to
121- # PROMPT_ONLY_MIN_CANDIDATES=256; with cap=8 the ceiling must also be 8.
140+ real_ids .append (_add (store , eng .embedder , wid , None , f"fact { i } " ))
141+
142+ index = _RecordingIndex (hit_count = 4 , real_ids = real_ids )
143+ eng .index = index
144+
145+ eng .recall ("zzz-unmatched-query-zzz" , SearchFilter (workspace_id = wid ), k = 8 ,
146+ candidate_k = 1 , prompt_only = True , arm_config = vector_only )
147+
148+ # First arm: 1 + min(250, 1*3) = 4 (clamped at min(8, 4) = 4, then
149+ # floored at candidate_k=1, so 4). With the cap, the second-page
150+ # ceiling is min(256, 8) = 8. Without the cap the index would have
151+ # been queried with [4, 256]. The recording index returns 4 hits per
152+ # call (>= arm_candidate_k=4 so can_expand=True; < prompt_target=8
153+ # so the loop is forced to escalate to the ceiling).
154+ assert index .requested , "index was never queried -- test setup is broken"
155+ assert index .requested [0 ] == 4 , (
156+ f"first arm must be 4 (cap=8, candidate_k=1, prompt_only), "
157+ f"got { index .requested [0 ]} "
158+ )
159+ assert all (requested <= 8 for requested in index .requested ), (
160+ f"every index query must respect the cap=8 ceiling, "
161+ f"got { index .requested } "
162+ )
122163 assert max (index .requested ) <= 8
123- assert all (requested <= 8 for requested in index .requested )
124- # Without the cap the index would have been queried with [4, 256].
164+ # Sanity: the loop must have actually escalated, i.e. it must have
165+ # queried the index at least twice. If it queried only once, the
166+ # first arm satisfied prompt_target and the ceiling-clamp code path
167+ # was not exercised -- which is the vacuous case this test guards
168+ # against.
169+ assert len (index .requested ) >= 2 , (
170+ f"ceiling clamp must be exercised via the escalation loop; "
171+ f"only { len (index .requested )} index queries were recorded -- "
172+ f"the first arm already satisfied prompt_target and the cap "
173+ f"code path is not being run"
174+ )
125175
126176
127177def test_arm_candidate_k_cap_floor_protects_one_fact_corpus (monkeypatch ):
0 commit comments