22
33`build_context_preface` retrieves a number of extended (non-pinned) memories to
44inject into the model context. That count used to be hardcoded; it is now driven
5- by the `mem_recall_count` parameter so a user setting can control it.
5+ by the `mem_recall_count` parameter (resolved from a user pref) so a setting can
6+ control it.
67"""
78from types import SimpleNamespace
89from unittest .mock import MagicMock
910
11+ import pytest
12+
1013from src .chat_processor import ChatProcessor
14+ from routes .chat_helpers import (
15+ resolve_memory_recall_count ,
16+ MEMORY_RECALL_COUNT_MIN ,
17+ MEMORY_RECALL_COUNT_MAX ,
18+ )
1119
1220
13- def _processor_with_memories (monkeypatch , n_extended = 12 ):
14- """Build a ChatProcessor whose memory store returns `n_extended` non-pinned memories."""
21+ def _processor_with_memories (n_extended = 12 ):
22+ """A ChatProcessor whose memory store returns `n_extended` non-pinned memories."""
1523 mem_entries = [
1624 {"id" : str (i ), "text" : f"fact { i } " , "category" : "fact" , "pinned" : False }
1725 for i in range (n_extended )
1826 ]
1927 memory_manager = MagicMock ()
2028 memory_manager .load .return_value = mem_entries
21- processor = ChatProcessor (memory_manager = memory_manager , personal_docs_manager = MagicMock ())
22- return processor
29+ return ChatProcessor (memory_manager = memory_manager , personal_docs_manager = MagicMock ())
2330
2431
25- def test_mem_recall_count_drives_retrieval_k (monkeypatch ):
26- """A non-default mem_recall_count is forwarded to the retrieval step as k."""
27- processor = _processor_with_memories (monkeypatch )
32+ def _capture_k (monkeypatch , processor ):
2833 captured = {}
2934
30- def fake_retrieve (message , mem_entries , k = 5 ):
35+ def fake_retrieve (message , mem_entries , k ):
3136 captured ["k" ] = k
3237 return []
3338
3439 monkeypatch .setattr (processor , "_hybrid_retrieve" , fake_retrieve )
40+ return captured
41+
42+
43+ # ── build_context_preface forwards the count to retrieval ───────────────────
44+
45+ def test_mem_recall_count_drives_retrieval_k (monkeypatch ):
46+ """A non-default mem_recall_count is forwarded to the retrieval step as k."""
47+ processor = _processor_with_memories ()
48+ captured = _capture_k (monkeypatch , processor )
3549 session = SimpleNamespace (endpoint_url = "http://local" , model = "test" , headers = {})
3650
3751 processor .build_context_preface (
38- message = "hello" ,
39- session = session ,
40- use_web = False ,
41- use_rag = False ,
42- use_memory = True ,
43- use_skills = False ,
52+ message = "hello" , session = session ,
53+ use_web = False , use_rag = False , use_memory = True , use_skills = False ,
4454 mem_recall_count = 7 ,
4555 )
4656
@@ -49,23 +59,44 @@ def fake_retrieve(message, mem_entries, k=5):
4959
5060def test_mem_recall_count_defaults_to_current_behavior (monkeypatch ):
5161 """Omitting mem_recall_count preserves the previous hardcoded count (3)."""
52- processor = _processor_with_memories (monkeypatch )
53- captured = {}
54-
55- def fake_retrieve (message , mem_entries , k = 5 ):
56- captured ["k" ] = k
57- return []
58-
59- monkeypatch .setattr (processor , "_hybrid_retrieve" , fake_retrieve )
62+ processor = _processor_with_memories ()
63+ captured = _capture_k (monkeypatch , processor )
6064 session = SimpleNamespace (endpoint_url = "http://local" , model = "test" , headers = {})
6165
6266 processor .build_context_preface (
63- message = "hello" ,
64- session = session ,
65- use_web = False ,
66- use_rag = False ,
67- use_memory = True ,
68- use_skills = False ,
67+ message = "hello" , session = session ,
68+ use_web = False , use_rag = False , use_memory = True , use_skills = False ,
6969 )
7070
7171 assert captured .get ("k" ) == 3
72+
73+
74+ # ── resolve_memory_recall_count: pref resolution + clamp + malformed input ──
75+
76+ def test_resolve_uses_valid_pref ():
77+ assert resolve_memory_recall_count ({"memory_recall_count" : 10 }) == 10
78+
79+
80+ def test_resolve_missing_pref_falls_back_to_default ():
81+ # No pref -> global setting -> historical default of 3.
82+ assert resolve_memory_recall_count ({}) == 3
83+
84+
85+ @pytest .mark .parametrize ("bad" , [None , "abc" , float ("nan" ), [1 ], {}])
86+ def test_resolve_malformed_pref_falls_back_to_3 (bad ):
87+ """A non-int / NaN / null pref must not throw or disable recall — falls back to 3."""
88+ assert resolve_memory_recall_count ({"memory_recall_count" : bad }) == 3
89+
90+
91+ def test_resolve_clamps_low_so_recall_is_never_disabled ():
92+ # k<=0 would make `queued >= k`-style logic recall nothing; clamp to the floor.
93+ assert resolve_memory_recall_count ({"memory_recall_count" : 0 }) == MEMORY_RECALL_COUNT_MIN
94+ assert resolve_memory_recall_count ({"memory_recall_count" : - 5 }) == MEMORY_RECALL_COUNT_MIN
95+
96+
97+ def test_resolve_clamps_high_so_prompt_cannot_balloon ():
98+ assert resolve_memory_recall_count ({"memory_recall_count" : 9999 }) == MEMORY_RECALL_COUNT_MAX
99+
100+
101+ def test_resolve_truncates_float ():
102+ assert resolve_memory_recall_count ({"memory_recall_count" : 2.9 }) == 2
0 commit comments