Mirror the worker's LRU-eviction ACTIVE_RUNS liveness check in _evict_session_agent so truncate/clear/model-switch can't close the SessionDB out from under an in-flight turn on the same session (#5096 Bug D follow-up). The cache handle still drops (harmless — worker holds a local ref); only the lifecycle commit + _session_db.close() are skipped while a run is active. Co-authored-by: claw-io <claw-io@users.noreply.github.com>
81 lines
3.0 KiB
Python
81 lines
3.0 KiB
Python
"""Regression: _evict_session_agent must not close a live session's _session_db.
|
|
|
|
Bug-D follow-up (#5096): truncate/clear/model-switch all call
|
|
api.config._evict_session_agent(). The worker assigns agent._session_db at run
|
|
start, so eviction must consult ACTIVE_RUNS (the authoritative liveness signal,
|
|
same as the worker's own LRU-eviction guard) and skip the lifecycle commit +
|
|
_session_db.close() while a run is in flight on that session. Otherwise a
|
|
truncate racing an in-flight turn on the same session (reachable via a second
|
|
client / direct API; the UI gates it behind S.busy) closes the SessionDB the
|
|
running worker is still persisting through.
|
|
"""
|
|
|
|
import api.config as config
|
|
|
|
|
|
class _FakeSessionDB:
|
|
def __init__(self):
|
|
self.closed = False
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
|
|
class _FakeAgent:
|
|
def __init__(self):
|
|
self._session_db = _FakeSessionDB()
|
|
|
|
|
|
def _seed_cache(session_id, agent):
|
|
with config.SESSION_AGENT_CACHE_LOCK:
|
|
config.SESSION_AGENT_CACHE[session_id] = (agent, "sig")
|
|
|
|
|
|
def _clear_active_runs():
|
|
with config.ACTIVE_RUNS_LOCK:
|
|
config.ACTIVE_RUNS.clear()
|
|
|
|
|
|
def test_evict_skips_session_db_close_when_run_active(monkeypatch):
|
|
"""A live run on the session => the cache handle drops but the DB stays open."""
|
|
sid = "live-session-evict-guard"
|
|
agent = _FakeAgent()
|
|
_seed_cache(sid, agent)
|
|
_clear_active_runs()
|
|
config.register_active_run("stream-xyz", session_id=sid)
|
|
try:
|
|
config._evict_session_agent(sid)
|
|
# The agent handle is removed from the cache (harmless — the worker
|
|
# holds its own local ref) ...
|
|
with config.SESSION_AGENT_CACHE_LOCK:
|
|
assert sid not in config.SESSION_AGENT_CACHE
|
|
# ... but the live worker's SessionDB must NOT be closed.
|
|
assert agent._session_db.closed is False
|
|
finally:
|
|
_clear_active_runs()
|
|
with config.SESSION_AGENT_CACHE_LOCK:
|
|
config.SESSION_AGENT_CACHE.pop(sid, None)
|
|
|
|
|
|
def test_evict_closes_session_db_when_no_run_active(monkeypatch):
|
|
"""No live run => normal eviction closes the SessionDB (idle path unchanged)."""
|
|
sid = "idle-session-evict-guard"
|
|
agent = _FakeAgent()
|
|
_seed_cache(sid, agent)
|
|
_clear_active_runs()
|
|
|
|
# Neutralize the lifecycle commit machinery so the test isolates the
|
|
# ACTIVE_RUNS guard + close decision (no uncommitted work => should_close).
|
|
monkeypatch.setattr("api.session_lifecycle.has_uncommitted_work", lambda *_a, **_k: False)
|
|
monkeypatch.setattr("api.session_lifecycle.unregister_agent", lambda *_a, **_k: None)
|
|
monkeypatch.setattr("api.session_lifecycle.discard_session", lambda *_a, **_k: None)
|
|
try:
|
|
config._evict_session_agent(sid)
|
|
with config.SESSION_AGENT_CACHE_LOCK:
|
|
assert sid not in config.SESSION_AGENT_CACHE
|
|
assert agent._session_db.closed is True
|
|
finally:
|
|
_clear_active_runs()
|
|
with config.SESSION_AGENT_CACHE_LOCK:
|
|
config.SESSION_AGENT_CACHE.pop(sid, None)
|