agentcache / tests /test_debounce.py
Yash030's picture
feat: upscale & UX overhaul — blueprint split, debounce, tests, viewer enhancements, DX improvements
4d5727a
Raw
History Blame Contribute Delete
5.4 kB
"""A4.3 — Unit tests for IndexPersistence debounce behavior.
Tests:
- 100 rapid schedule_save() calls result in exactly 1 save() call.
- flush() triggers immediate save without waiting for debounce timer.
"""
import sys
import os
import time
import threading
import unittest.mock as mock
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
import pytest
from db import StateKV
from search import SearchIndex, VectorIndex
from functions import IndexPersistence
# Speed up debounce for tests
FAST_DEBOUNCE = 0.05
def make_kv(tmp_path):
return StateKV(db_path=str(tmp_path / 'test_debounce.db'))
class TestDebounce:
def test_100_rapid_calls_result_in_1_save(self, tmp_path):
"""100 rapid schedule_save() calls must fire exactly 1 save()."""
kv = make_kv(tmp_path)
bm25 = SearchIndex()
vector = VectorIndex()
persistence = IndexPersistence(kv, bm25, vector)
persistence.DEBOUNCE_SECONDS = FAST_DEBOUNCE
save_call_count = [0]
original_save = persistence.save
def counting_save():
save_call_count[0] += 1
original_save()
with mock.patch.object(persistence, 'save', side_effect=counting_save):
for _ in range(100):
persistence.schedule_save()
# Wait for the debounce timer to fire (2× debounce window is plenty)
time.sleep(FAST_DEBOUNCE * 4)
assert save_call_count[0] == 1, (
f"Expected exactly 1 save() call; got {save_call_count[0]}"
)
def test_rapid_calls_with_dirty_bm25(self, tmp_path):
"""schedule_save() fires exactly once even when BM25 is dirty."""
kv = make_kv(tmp_path)
bm25 = SearchIndex()
vector = VectorIndex()
# Add a doc so the index is dirty
bm25.add({
'id': 'obs_test1',
'sessionId': 'sess1',
'title': 'hello world',
'facts': [],
'concepts': [],
'files': [],
'type': 'other',
})
persistence = IndexPersistence(kv, bm25, vector)
persistence.DEBOUNCE_SECONDS = FAST_DEBOUNCE
save_call_count = [0]
original_save = persistence.save
def counting_save():
save_call_count[0] += 1
original_save()
with mock.patch.object(persistence, 'save', side_effect=counting_save):
for _ in range(100):
persistence.schedule_save()
time.sleep(FAST_DEBOUNCE * 4)
assert save_call_count[0] == 1
def test_flush_triggers_immediate_save(self, tmp_path):
"""flush() must call save() immediately without waiting for the debounce timer."""
kv = make_kv(tmp_path)
bm25 = SearchIndex()
vector = VectorIndex()
persistence = IndexPersistence(kv, bm25, vector)
persistence.DEBOUNCE_SECONDS = 60.0 # very long timer — flush must bypass it
save_call_count = [0]
original_save = persistence.save
def counting_save():
save_call_count[0] += 1
original_save()
with mock.patch.object(persistence, 'save', side_effect=counting_save):
persistence.schedule_save()
# Timer is set but hasn't fired yet (60s window)
assert save_call_count[0] == 0, "save() should not have been called yet"
# flush() must cancel the timer and call save() synchronously
persistence.flush()
assert save_call_count[0] == 1, (
f"flush() should trigger exactly 1 save(); got {save_call_count[0]}"
)
def test_flush_after_no_pending_save_is_safe(self, tmp_path):
"""flush() with no pending timer should still call save() once."""
kv = make_kv(tmp_path)
bm25 = SearchIndex()
vector = VectorIndex()
persistence = IndexPersistence(kv, bm25, vector)
save_call_count = [0]
original_save = persistence.save
def counting_save():
save_call_count[0] += 1
original_save()
with mock.patch.object(persistence, 'save', side_effect=counting_save):
persistence.flush()
assert save_call_count[0] == 1
def test_subsequent_schedule_after_fire_starts_new_timer(self, tmp_path):
"""Two bursts of saves separated by more than DEBOUNCE_SECONDS should fire 2 saves."""
kv = make_kv(tmp_path)
bm25 = SearchIndex()
vector = VectorIndex()
persistence = IndexPersistence(kv, bm25, vector)
persistence.DEBOUNCE_SECONDS = FAST_DEBOUNCE
save_call_count = [0]
original_save = persistence.save
def counting_save():
save_call_count[0] += 1
original_save()
with mock.patch.object(persistence, 'save', side_effect=counting_save):
# First burst
for _ in range(10):
persistence.schedule_save()
# Wait for first timer to fire
time.sleep(FAST_DEBOUNCE * 4)
# Second burst
for _ in range(10):
persistence.schedule_save()
# Wait for second timer to fire
time.sleep(FAST_DEBOUNCE * 4)
assert save_call_count[0] == 2, (
f"Expected 2 save() calls for two separate bursts; got {save_call_count[0]}"
)