@@ -126,3 +126,104 @@ impl Default for CorrectionMemory {
126126 Self :: new ( )
127127 }
128128}
129+
130+ #[ cfg( test) ]
131+ mod tests {
132+ use super :: * ;
133+
134+ #[ test]
135+ fn record_and_check_below_threshold ( ) {
136+ let mut mem = CorrectionMemory :: new ( ) ;
137+ let hash = CorrectionMemory :: context_hash ( Some ( "the" ) , Some ( "quick" ) ) ;
138+ mem. record ( hash, "fox" , "cat" ) ;
139+ mem. record ( hash, "fox" , "cat" ) ;
140+ // Only 2 corrections — threshold is 3, should not suppress yet
141+ assert ! ( mem. check( hash, "fox" ) . is_none( ) ) ;
142+ }
143+
144+ #[ test]
145+ fn record_and_check_at_threshold ( ) {
146+ let mut mem = CorrectionMemory :: new ( ) ;
147+ let hash = CorrectionMemory :: context_hash ( Some ( "the" ) , Some ( "quick" ) ) ;
148+ mem. record ( hash, "fox" , "cat" ) ;
149+ mem. record ( hash, "fox" , "cat" ) ;
150+ mem. record ( hash, "fox" , "cat" ) ;
151+ // 3 corrections — should now suppress
152+ let result = mem. check ( hash, "fox" ) ;
153+ assert_eq ! ( result, Some ( "cat" . to_string( ) ) ) ;
154+ }
155+
156+ #[ test]
157+ fn context_hash_is_deterministic ( ) {
158+ let h1 = CorrectionMemory :: context_hash ( Some ( "hello" ) , Some ( "world" ) ) ;
159+ let h2 = CorrectionMemory :: context_hash ( Some ( "hello" ) , Some ( "world" ) ) ;
160+ assert_eq ! ( h1, h2) ;
161+ }
162+
163+ #[ test]
164+ fn context_hash_differs_for_different_context ( ) {
165+ let h1 = CorrectionMemory :: context_hash ( Some ( "hello" ) , Some ( "world" ) ) ;
166+ let h2 = CorrectionMemory :: context_hash ( Some ( "foo" ) , Some ( "bar" ) ) ;
167+ assert_ne ! ( h1, h2) ;
168+ }
169+
170+ #[ test]
171+ fn context_hash_none_context ( ) {
172+ let h1 = CorrectionMemory :: context_hash ( None , None ) ;
173+ let h2 = CorrectionMemory :: context_hash ( None , None ) ;
174+ assert_eq ! ( h1, h2) ;
175+ }
176+
177+ #[ test]
178+ fn different_predicted_prefixes_tracked_independently ( ) {
179+ let mut mem = CorrectionMemory :: new ( ) ;
180+ let hash = CorrectionMemory :: context_hash ( Some ( "the" ) , None ) ;
181+ for _ in 0 ..3 {
182+ mem. record ( hash, "wrong1" , "right" ) ;
183+ }
184+ // "wrong2" has zero corrections — should not suppress
185+ assert ! ( mem. check( hash, "wrong2" ) . is_none( ) ) ;
186+ // "wrong1" has 3 corrections — should suppress
187+ assert ! ( mem. check( hash, "wrong1" ) . is_some( ) ) ;
188+ }
189+
190+ #[ test]
191+ fn actual_updates_on_new_correction ( ) {
192+ let mut mem = CorrectionMemory :: new ( ) ;
193+ let hash = CorrectionMemory :: context_hash ( Some ( "a" ) , Some ( "b" ) ) ;
194+ mem. record ( hash, "pred" , "first" ) ;
195+ mem. record ( hash, "pred" , "first" ) ;
196+ // Update "actual" to something different on the 3rd correction
197+ mem. record ( hash, "pred" , "second" ) ;
198+ let result = mem. check ( hash, "pred" ) ;
199+ assert_eq ! ( result, Some ( "second" . to_string( ) ) ) ;
200+ }
201+
202+ #[ test]
203+ fn snapshot_round_trip ( ) {
204+ let mut mem = CorrectionMemory :: new ( ) ;
205+ let hash = CorrectionMemory :: context_hash ( Some ( "x" ) , Some ( "y" ) ) ;
206+ for _ in 0 ..3 {
207+ mem. record ( hash, "bad" , "good" ) ;
208+ }
209+ let snapshot = mem. to_snapshot ( ) ;
210+ assert_eq ! ( snapshot. entries. len( ) , 1 ) ;
211+ let restored = CorrectionMemory :: from_snapshot ( & snapshot) ;
212+ let snap2 = restored. to_snapshot ( ) ;
213+ assert_eq ! ( snap2. entries. len( ) , 1 ) ;
214+ assert_eq ! ( snap2. entries[ 0 ] . count, 3 ) ;
215+ }
216+
217+ #[ test]
218+ fn lru_eviction_keeps_within_capacity ( ) {
219+ // Use a small capacity by manually filling
220+ let mut mem = CorrectionMemory :: new ( ) ;
221+ // Fill to just above max_entries (500)
222+ for i in 0 ..502u64 {
223+ let hash = i;
224+ mem. record ( hash, "pred" , "actual" ) ;
225+ }
226+ // After eviction, entries should be <= max_entries
227+ assert ! ( mem. entries. len( ) <= 500 ) ;
228+ }
229+ }
0 commit comments