|
| 1 | +"""Tests for the opt-in ``ENGRAPHIS_RECALL_NARROW_ARM`` latency knob. |
| 2 | +
|
| 3 | +B2's 4th-pass review found the PR #171 prompt-only widening |
| 4 | +(``candidate_k + min(250, candidate_k*3)``) costs ~5x more matrix-vector |
| 5 | +work on a 49-fact corpus at k=8 (32ms -> 175ms). That regression was the |
| 6 | +user-accepted trade-off for keeping recall quality on small-k callers, |
| 7 | +but operators who care more about latency than top-of-list precision can |
| 8 | +opt in to a narrower arm by setting ``ENGRAPHIS_RECALL_NARROW_ARM=1``. |
| 9 | +
|
| 10 | +When the knob is enabled, the first prompt-only arm is clamped to |
| 11 | +``max(k, min(50, candidate_k*2))`` and the escalation ceiling is clamped |
| 12 | +to ``min(PROMPT_ONLY_MAX_CANDIDATES, candidate_k*4)`` so the second page |
| 13 | +does not silently undo the savings. The narrow arm is gated on k <= 20 |
| 14 | +because larger-k callers still need the full widening to find approved |
| 15 | +evidence. |
| 16 | +
|
| 17 | +Default behavior (env var unset / 0 / empty) is unchanged — the wider |
| 18 | +arm from PR #171 is preserved verbatim. |
| 19 | +
|
| 20 | +These tests prove both halves of the contract: the env var is honored |
| 21 | +when set, and the wider arm is the default when it is not. |
| 22 | +""" |
| 23 | +from __future__ import annotations |
| 24 | + |
| 25 | +from engraphis.backends import DeterministicEmbedder |
| 26 | +from engraphis.backends.reranker import IdentityReranker |
| 27 | +from engraphis.core.interfaces import MemoryRecord, SearchFilter |
| 28 | +from engraphis.core.recall import RecallEngine |
| 29 | +from engraphis.core.store import Store |
| 30 | + |
| 31 | + |
| 32 | +class _SemanticTestEmbedder(DeterministicEmbedder): |
| 33 | + supports_semantic_search = True |
| 34 | + embedding_mode = "semantic" |
| 35 | + |
| 36 | + |
| 37 | +class _RecordingIndex: |
| 38 | + """Vector-index double that records every arm size it was queried with.""" |
| 39 | + |
| 40 | + def __init__(self): |
| 41 | + self.requested: list[int] = [] |
| 42 | + |
| 43 | + def search(self, query, k, *, filter=None): |
| 44 | + self.requested.append(int(k)) |
| 45 | + return [(f"mem_{i}", float(k - i)) for i in range(min(k, 4))] |
| 46 | + |
| 47 | + |
| 48 | +def _add(store, emb, wid, rid, text, **kw): |
| 49 | + provenance = dict(kw.get("provenance") or { |
| 50 | + "source": "test", "trusted": True, "review_state": "approved", |
| 51 | + }) |
| 52 | + if provenance.get("trusted") is True: |
| 53 | + provenance.setdefault("review_state", "approved") |
| 54 | + kw["provenance"] = provenance |
| 55 | + return store.add_memory(MemoryRecord( |
| 56 | + id="", content=text, workspace_id=wid, repo_id=rid, |
| 57 | + embedding=emb.embed([text])[0], **kw, |
| 58 | + )) |
| 59 | + |
| 60 | + |
| 61 | +def test_narrow_arm_opt_in_default_is_false(monkeypatch): |
| 62 | + """Without the env var the opt-in flag is False — the wider arm stays the |
| 63 | + default for every caller, including small-k ones.""" |
| 64 | + monkeypatch.delenv("ENGRAPHIS_RECALL_NARROW_ARM", raising=False) |
| 65 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 66 | + _RecordingIndex(), IdentityReranker()) |
| 67 | + assert eng._narrow_arm_opt_in is False |
| 68 | + |
| 69 | + |
| 70 | +def test_narrow_arm_opt_in_reads_env_var(monkeypatch): |
| 71 | + """Any non-empty, non-zero env value flips the opt-in flag; bad/zero |
| 72 | + values are ignored so a typo cannot silently change retrieval behavior.""" |
| 73 | + monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "1") |
| 74 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 75 | + _RecordingIndex(), IdentityReranker()) |
| 76 | + assert eng._narrow_arm_opt_in is True |
| 77 | + |
| 78 | + monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "true") |
| 79 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 80 | + _RecordingIndex(), IdentityReranker()) |
| 81 | + assert eng._narrow_arm_opt_in is True |
| 82 | + |
| 83 | + monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "0") |
| 84 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 85 | + _RecordingIndex(), IdentityReranker()) |
| 86 | + assert eng._narrow_arm_opt_in is False |
| 87 | + |
| 88 | + monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "false") |
| 89 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 90 | + _RecordingIndex(), IdentityReranker()) |
| 91 | + assert eng._narrow_arm_opt_in is False |
| 92 | + |
| 93 | + monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", " ") |
| 94 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 95 | + _RecordingIndex(), IdentityReranker()) |
| 96 | + assert eng._narrow_arm_opt_in is False |
| 97 | + |
| 98 | + |
| 99 | +def test_narrow_arm_opt_in_changes_first_arm_at_k_8(monkeypatch): |
| 100 | + """With the env var set, the first prompt-only arm at k=8 shrinks from |
| 101 | + 8 + min(250, 24) = 32 to min(50, 8*2) = 16. This is the latency win |
| 102 | + the knob is meant to unlock.""" |
| 103 | + monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "1") |
| 104 | + index = _RecordingIndex() |
| 105 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 106 | + index, IdentityReranker()) |
| 107 | + store = eng.store |
| 108 | + wid = store.get_or_create_workspace("w") |
| 109 | + for i in range(60): |
| 110 | + _add(store, eng.embedder, wid, None, f"fact {i}") |
| 111 | + |
| 112 | + result = eng.recall("fact 5", SearchFilter(workspace_id=wid), k=8, |
| 113 | + candidate_k=8, prompt_only=True) |
| 114 | + |
| 115 | + assert index.requested[0] == 16 # 8 + 24 (default) would have been 32 |
| 116 | + assert result.candidate_k_used == 16 |
| 117 | + # The result must still be non-empty: the narrow arm must not crash |
| 118 | + # recall on a trusted-only corpus. |
| 119 | + assert result.count >= 1 |
| 120 | + |
| 121 | + |
| 122 | +def test_narrow_arm_does_not_change_default(monkeypatch): |
| 123 | + """Without the env var the wider arm is preserved. This is the |
| 124 | + user-accepted trade-off: 5x latency for top-of-list recall.""" |
| 125 | + monkeypatch.delenv("ENGRAPHIS_RECALL_NARROW_ARM", raising=False) |
| 126 | + index = _RecordingIndex() |
| 127 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 128 | + index, IdentityReranker()) |
| 129 | + store = eng.store |
| 130 | + wid = store.get_or_create_workspace("w") |
| 131 | + for i in range(60): |
| 132 | + _add(store, eng.embedder, wid, None, f"fact {i}") |
| 133 | + |
| 134 | + result = eng.recall("fact 5", SearchFilter(workspace_id=wid), k=8, |
| 135 | + candidate_k=8, prompt_only=True) |
| 136 | + |
| 137 | + # PR #171 default: 8 + min(250, 8*3) = 8 + 24 = 32 |
| 138 | + assert index.requested[0] == 32 |
| 139 | + assert result.candidate_k_used == 32 |
| 140 | + |
| 141 | + |
| 142 | +def test_narrow_arm_only_applies_to_small_k(monkeypatch): |
| 143 | + """The narrow arm is gated on k <= 20. A k=50 caller must still see |
| 144 | + the wider arm even with the env var set — that is the caller class |
| 145 | + that motivated the widening in the first place.""" |
| 146 | + monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "1") |
| 147 | + index = _RecordingIndex() |
| 148 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 149 | + index, IdentityReranker()) |
| 150 | + store = eng.store |
| 151 | + wid = store.get_or_create_workspace("w") |
| 152 | + for i in range(60): |
| 153 | + _add(store, eng.embedder, wid, None, f"fact {i}") |
| 154 | + |
| 155 | + eng.recall("fact 5", SearchFilter(workspace_id=wid), k=50, |
| 156 | + candidate_k=50, prompt_only=True) |
| 157 | + |
| 158 | + # k=50 is above the gate; wider arm preserved: 50 + min(250, 150) = 200. |
| 159 | + assert index.requested[0] == 200 |
| 160 | + |
| 161 | + # k=20 is at the boundary and should still be narrow: 20*2 = 40. |
| 162 | + index2 = _RecordingIndex() |
| 163 | + eng2 = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 164 | + index2, IdentityReranker()) |
| 165 | + eng2.recall("fact 5", SearchFilter(workspace_id=wid), k=20, |
| 166 | + candidate_k=20, prompt_only=True) |
| 167 | + assert index2.requested[0] == 40 |
| 168 | + |
| 169 | + # k=21 is just past the gate; wider arm preserved: 21 + min(250, 63) = 84. |
| 170 | + index3 = _RecordingIndex() |
| 171 | + eng3 = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 172 | + index3, IdentityReranker()) |
| 173 | + eng3.recall("fact 5", SearchFilter(workspace_id=wid), k=21, |
| 174 | + candidate_k=21, prompt_only=True) |
| 175 | + assert index3.requested[0] == 84 |
| 176 | + |
| 177 | + |
| 178 | +def test_narrow_arm_does_not_apply_to_non_prompt_only(monkeypatch): |
| 179 | + """The narrow arm is a prompt-only knob. A non-prompt recall with the |
| 180 | + env var set must behave exactly like the default, because the wider |
| 181 | + widening lives inside the ``if prompt_only`` block.""" |
| 182 | + monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "1") |
| 183 | + index = _RecordingIndex() |
| 184 | + eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256), |
| 185 | + index, IdentityReranker()) |
| 186 | + store = eng.store |
| 187 | + wid = store.get_or_create_workspace("w") |
| 188 | + for i in range(60): |
| 189 | + _add(store, eng.embedder, wid, None, f"fact {i}") |
| 190 | + |
| 191 | + # include_untrusted=True forces prompt_only=False regardless of the |
| 192 | + # env var; the first arm must be the raw candidate_k with no widening. |
| 193 | + eng.recall("fact 5", SearchFilter(workspace_id=wid), k=8, |
| 194 | + candidate_k=8, prompt_only=False, include_untrusted=True) |
| 195 | + assert index.requested[0] == 8 |
0 commit comments