fix(session): avoid blocking large chat cleanup
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from nanobot.agent.loop import AgentLoop
|
||||
import nanobot.agent.memory as memory_module
|
||||
from nanobot.agent.loop import AgentLoop
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.providers.base import LLMResponse
|
||||
|
||||
@@ -188,3 +189,36 @@ async def test_preflight_consolidation_before_llm_call(tmp_path, monkeypatch) ->
|
||||
assert "consolidate" in order
|
||||
assert "llm" in order
|
||||
assert order.index("consolidate") < order.index("llm")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_slow_preflight_consolidation_continues_in_background(tmp_path, monkeypatch) -> None:
|
||||
order: list[str] = []
|
||||
|
||||
loop = _make_loop(tmp_path, estimated_tokens=0, context_window_tokens=200)
|
||||
monkeypatch.setattr(loop, "_PREFLIGHT_CONSOLIDATION_BUDGET_SECONDS", 0.01)
|
||||
|
||||
release = asyncio.Event()
|
||||
|
||||
async def slow_consolidation(_session):
|
||||
order.append("consolidate-start")
|
||||
await release.wait()
|
||||
order.append("consolidate-end")
|
||||
|
||||
async def track_llm(*args, **kwargs):
|
||||
order.append("llm")
|
||||
return LLMResponse(content="ok", tool_calls=[])
|
||||
|
||||
loop.memory_consolidator.maybe_consolidate_by_tokens = slow_consolidation # type: ignore[method-assign]
|
||||
loop.provider.chat_with_retry = track_llm
|
||||
|
||||
await loop.process_direct("hello", session_key="cli:test")
|
||||
|
||||
assert "consolidate-start" in order
|
||||
assert "llm" in order
|
||||
assert "consolidate-end" not in order
|
||||
|
||||
release.set()
|
||||
await loop.close_mcp()
|
||||
|
||||
assert "consolidate-end" in order
|
||||
|
||||
104
tests/test_session_manager_persistence.py
Normal file
104
tests/test_session_manager_persistence.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from nanobot.session.manager import SessionManager
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict]:
|
||||
return [
|
||||
json.loads(line)
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
|
||||
|
||||
def test_save_appends_only_new_messages(tmp_path: Path) -> None:
|
||||
manager = SessionManager(tmp_path)
|
||||
session = manager.get_or_create("qq:test")
|
||||
session.add_message("user", "hello")
|
||||
session.add_message("assistant", "hi")
|
||||
manager.save(session)
|
||||
|
||||
path = manager._get_session_path(session.key)
|
||||
original_text = path.read_text(encoding="utf-8")
|
||||
|
||||
session.add_message("user", "next")
|
||||
manager.save(session)
|
||||
|
||||
lines = _read_jsonl(path)
|
||||
assert path.read_text(encoding="utf-8").startswith(original_text)
|
||||
assert sum(1 for line in lines if line.get("_type") == "metadata") == 1
|
||||
assert [line["content"] for line in lines if line.get("role")] == ["hello", "hi", "next"]
|
||||
|
||||
|
||||
def test_save_appends_metadata_checkpoint_without_rewriting_history(tmp_path: Path) -> None:
|
||||
manager = SessionManager(tmp_path)
|
||||
session = manager.get_or_create("qq:test")
|
||||
session.add_message("user", "hello")
|
||||
session.add_message("assistant", "hi")
|
||||
manager.save(session)
|
||||
|
||||
path = manager._get_session_path(session.key)
|
||||
original_text = path.read_text(encoding="utf-8")
|
||||
|
||||
session.last_consolidated = 2
|
||||
manager.save(session)
|
||||
|
||||
lines = _read_jsonl(path)
|
||||
assert path.read_text(encoding="utf-8").startswith(original_text)
|
||||
assert sum(1 for line in lines if line.get("_type") == "metadata") == 2
|
||||
assert lines[-1]["_type"] == "metadata"
|
||||
assert lines[-1]["last_consolidated"] == 2
|
||||
|
||||
manager.invalidate(session.key)
|
||||
reloaded = manager.get_or_create("qq:test")
|
||||
assert reloaded.last_consolidated == 2
|
||||
assert [message["content"] for message in reloaded.messages] == ["hello", "hi"]
|
||||
|
||||
|
||||
def test_clear_rewrites_session_file(tmp_path: Path) -> None:
|
||||
manager = SessionManager(tmp_path)
|
||||
session = manager.get_or_create("qq:test")
|
||||
session.add_message("user", "hello")
|
||||
session.add_message("assistant", "hi")
|
||||
manager.save(session)
|
||||
|
||||
path = manager._get_session_path(session.key)
|
||||
session.clear()
|
||||
manager.save(session)
|
||||
|
||||
lines = _read_jsonl(path)
|
||||
assert len(lines) == 1
|
||||
assert lines[0]["_type"] == "metadata"
|
||||
assert lines[0]["last_consolidated"] == 0
|
||||
|
||||
manager.invalidate(session.key)
|
||||
reloaded = manager.get_or_create("qq:test")
|
||||
assert reloaded.messages == []
|
||||
assert reloaded.last_consolidated == 0
|
||||
|
||||
|
||||
def test_list_sessions_uses_file_mtime_for_append_only_updates(tmp_path: Path) -> None:
|
||||
manager = SessionManager(tmp_path)
|
||||
session = manager.get_or_create("qq:test")
|
||||
session.add_message("user", "hello")
|
||||
manager.save(session)
|
||||
|
||||
path = manager._get_session_path(session.key)
|
||||
stale_time = time.time() - 3600
|
||||
os.utime(path, (stale_time, stale_time))
|
||||
|
||||
before = datetime.fromisoformat(manager.list_sessions()[0]["updated_at"])
|
||||
assert before.timestamp() < time.time() - 3000
|
||||
|
||||
session.add_message("assistant", "hi")
|
||||
manager.save(session)
|
||||
|
||||
after = datetime.fromisoformat(manager.list_sessions()[0]["updated_at"])
|
||||
assert after > before
|
||||
|
||||
Reference in New Issue
Block a user