refactor(agent): require runtime for consolidation

This commit is contained in:
chengyongru
2026-07-10 17:54:34 +08:00
committed by Xubin Ren
parent 5bd3d1e0af
commit c9d3e74342
18 changed files with 486 additions and 214 deletions
+48 -15
View File
@@ -9,6 +9,10 @@ from nanobot.agent.autocompact import AutoCompact
from nanobot.session.manager import Session, SessionManager
def _runtime():
return MagicMock(name="runtime")
def _make_session(
key: str = "cli:test",
messages: list | None = None,
@@ -193,7 +197,7 @@ class TestCheckExpired:
mock_sm.list_sessions.return_value = []
ac.sessions = mock_sm
scheduler = MagicMock()
ac.check_expired(scheduler)
ac.check_expired(scheduler, _runtime)
scheduler.assert_not_called()
def test_expired_session_schedules_background(self):
@@ -213,10 +217,36 @@ class TestCheckExpired:
scheduled.append(coro)
coro.close()
ac.check_expired(scheduler)
ac.check_expired(scheduler, _runtime)
assert len(scheduled) == 1
assert "cli:old" in ac._archiving
@pytest.mark.asyncio
async def test_runtime_is_captured_before_background_starts(self):
ac = _make_autocompact(ttl=15)
old_dt = datetime.now() - timedelta(minutes=20)
session = _make_session("cli:old", updated_at=old_dt)
_add_turns(session, 5)
ac.sessions.list_sessions.return_value = [
{"key": "cli:old", "updated_at": old_dt.isoformat()}
]
ac.sessions.get_or_create.return_value = session
admitted = _runtime()
replacement = _runtime()
resolve_runtime = MagicMock(return_value=admitted)
scheduled = []
ac.check_expired(scheduled.append, resolve_runtime)
resolve_runtime.return_value = replacement
await scheduled[0]
resolve_runtime.assert_called_once_with()
ac.consolidator.compact_idle_session.assert_awaited_once_with(
"cli:old",
runtime=admitted,
max_suffix=ac._RECENT_SUFFIX_MESSAGES,
)
def test_active_session_key_skips(self):
"""Session in active_session_keys should be skipped."""
ac = _make_autocompact(ttl=15)
@@ -225,7 +255,7 @@ class TestCheckExpired:
mock_sm.list_sessions.return_value = [{"key": "cli:busy", "updated_at": old_ts}]
ac.sessions = mock_sm
scheduler = MagicMock()
ac.check_expired(scheduler, active_session_keys={"cli:busy"})
ac.check_expired(scheduler, _runtime, active_session_keys={"cli:busy"})
scheduler.assert_not_called()
def test_session_already_in_archiving_skips(self):
@@ -237,7 +267,7 @@ class TestCheckExpired:
ac.sessions = mock_sm
ac._archiving.add("cli:dup")
scheduler = MagicMock()
ac.check_expired(scheduler)
ac.check_expired(scheduler, _runtime)
scheduler.assert_not_called()
def test_session_with_no_key_skips(self):
@@ -247,7 +277,7 @@ class TestCheckExpired:
mock_sm.list_sessions.return_value = [{"key": "", "updated_at": "old"}]
ac.sessions = mock_sm
scheduler = MagicMock()
ac.check_expired(scheduler)
ac.check_expired(scheduler, _runtime)
scheduler.assert_not_called()
def test_session_with_missing_key_field_skips(self):
@@ -257,7 +287,7 @@ class TestCheckExpired:
mock_sm.list_sessions.return_value = [{"updated_at": "old"}]
ac.sessions = mock_sm
scheduler = MagicMock()
ac.check_expired(scheduler)
ac.check_expired(scheduler, _runtime)
scheduler.assert_not_called()
def test_dream_session_skips(self):
@@ -271,7 +301,7 @@ class TestCheckExpired:
ac.sessions = mock_sm
scheduler = MagicMock()
ac.check_expired(scheduler)
ac.check_expired(scheduler, _runtime)
scheduler.assert_not_called()
assert "dream:20260602-155256" not in ac._archiving
@@ -290,7 +320,7 @@ class TestCheckExpired:
ac.sessions = mock_sm
scheduler = MagicMock()
ac.check_expired(scheduler)
ac.check_expired(scheduler, _runtime)
scheduler.assert_not_called()
@@ -310,10 +340,13 @@ class TestArchiveDelegates:
ac.sessions = mock_sm
ac.consolidator.compact_idle_session = AsyncMock(return_value="Summary.")
await ac._archive("cli:test")
runtime = _runtime()
await ac._archive("cli:test", runtime=runtime)
ac.consolidator.compact_idle_session.assert_awaited_once_with(
"cli:test", ac._RECENT_SUFFIX_MESSAGES,
"cli:test",
runtime=runtime,
max_suffix=ac._RECENT_SUFFIX_MESSAGES,
)
@pytest.mark.asyncio
@@ -322,7 +355,7 @@ class TestArchiveDelegates:
ac.consolidator.compact_idle_session = AsyncMock(return_value="Summary.")
ac._archiving.add("dream:20260602-155256")
await ac._archive("dream:20260602-155256")
await ac._archive("dream:20260602-155256", runtime=_runtime())
ac.consolidator.compact_idle_session.assert_not_awaited()
assert "dream:20260602-155256" not in ac._archiving
@@ -338,7 +371,7 @@ class TestArchiveDelegates:
ac.sessions = mock_sm
ac.consolidator.compact_idle_session = AsyncMock(return_value="Hello.")
await ac._archive("cli:test")
await ac._archive("cli:test", runtime=_runtime())
entry = ac._summaries.get("cli:test")
assert entry is not None
@@ -351,7 +384,7 @@ class TestArchiveDelegates:
ac.sessions = mock_sm
ac.consolidator.compact_idle_session = AsyncMock(return_value="")
await ac._archive("cli:test")
await ac._archive("cli:test", runtime=_runtime())
assert "cli:test" not in ac._summaries
@@ -362,7 +395,7 @@ class TestArchiveDelegates:
ac.sessions = mock_sm
ac.consolidator.compact_idle_session = AsyncMock(return_value="(nothing)")
await ac._archive("cli:test")
await ac._archive("cli:test", runtime=_runtime())
assert "cli:test" not in ac._summaries
@@ -374,7 +407,7 @@ class TestArchiveDelegates:
ac.consolidator.compact_idle_session = AsyncMock(side_effect=RuntimeError("fail"))
ac._archiving.add("cli:test")
await ac._archive("cli:test")
await ac._archive("cli:test", runtime=_runtime())
assert "cli:test" not in ac._archiving