"""Regression tests for #2863 missing session-index background rebuild.""" from __future__ import annotations import json import time def test_missing_index_starts_background_rebuild_while_preserving_first_scan(monkeypatch, tmp_path): import api.models as models from api.models import all_sessions # Hermetic isolation: a prior test in the same worker may have left the # background rebuild thread bookkeeping populated. Since #3884 the start # helper short-circuits when a rebuild thread for the same target is still # alive, so stale globals from another test could suppress the fresh thread # this test asserts on. Join any leftover thread and clear both globals so # this test only observes the thread IT triggers, regardless of run order. _stale = getattr(models, "_SESSION_INDEX_REBUILD_THREAD", None) if _stale is not None: try: _stale.join(timeout=5) except Exception: pass models._SESSION_INDEX_REBUILD_THREAD = None if hasattr(models, "_SESSION_INDEX_REBUILD_THREAD_TARGET"): models._SESSION_INDEX_REBUILD_THREAD_TARGET = None session_dir = tmp_path / "sessions" session_dir.mkdir(parents=True) monkeypatch.setattr(models, "SESSION_DIR", session_dir) monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json") models.SESSIONS.clear() for idx in range(3): payload = { "session_id": f"issue2863{idx}", "title": f"Session {idx}", "workspace": str(tmp_path), "model": "test-model", "messages": [{"role": "user", "content": f"hello {idx}", "timestamp": time.time() + idx}], "created_at": time.time() + idx, "updated_at": time.time() + idx, } (session_dir / f"issue2863{idx}.json").write_text(json.dumps(payload), encoding="utf-8") rows = all_sessions() assert {row["session_id"] for row in rows} == {"issue28630", "issue28631", "issue28632"} thread = models._SESSION_INDEX_REBUILD_THREAD # Fast runners can complete the background rebuild and clear the global # thread slot before this assertion observes it. The invariant is that the # first scan remains correct and the index is rebuilt, not that the transient # thread object is still visible. if thread is not None: thread.join(timeout=5) assert not thread.is_alive() index = json.loads(models.SESSION_INDEX_FILE.read_text(encoding="utf-8")) assert {row["session_id"] for row in index} == {"issue28630", "issue28631", "issue28632"} assert {row["session_id"] for row in all_sessions()} == {"issue28630", "issue28631", "issue28632"}