79 lines
2.8 KiB
Python
79 lines
2.8 KiB
Python
"""Tests for the lightweight Consolidator — append-only to HISTORY.md."""
|
|||
|
|
|
||
|
|
import pytest
|
||
|
|
import asyncio
|
||
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
|
|
||
|
|
from nanobot.agent.memory import Consolidator, MemoryStore
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def store(tmp_path):
|
||
|
|
return MemoryStore(tmp_path)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_provider():
|
||
|
|
p = MagicMock()
|
||
|
|
p.chat_with_retry = AsyncMock()
|
||
|
|
return p
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def consolidator(store, mock_provider):
|
||
|
|
sessions = MagicMock()
|
||
|
|
sessions.save = MagicMock()
|
||
|
|
return Consolidator(
|
||
|
|
store=store,
|
||
|
|
provider=mock_provider,
|
||
|
|
model="test-model",
|
||
|
|
sessions=sessions,
|
||
|
|
context_window_tokens=1000,
|
||
|
|
build_messages=MagicMock(return_value=[]),
|
||
|
|
get_tool_definitions=MagicMock(return_value=[]),
|
||
|
|
max_completion_tokens=100,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestConsolidatorSummarize:
|
||
|
|
async def test_summarize_appends_to_history(self, consolidator, mock_provider, store):
|
||
|
|
"""Consolidator should call LLM to summarize, then append to HISTORY.md."""
|
||
|
|
mock_provider.chat_with_retry.return_value = MagicMock(
|
||
|
|
content="User fixed a bug in the auth module."
|
||
|
|
)
|
||
|
|
messages = [
|
||
|
|
{"role": "user", "content": "fix the auth bug"},
|
||
|
|
{"role": "assistant", "content": "Done, fixed the race condition."},
|
||
|
|
]
|
||
|
|
result = await consolidator.archive(messages)
|
||
|
|
assert result is True
|
||
|
|
entries = store.read_unprocessed_history(since_cursor=0)
|
||
|
|
assert len(entries) == 1
|
||
|
|
|
||
|
|
async def test_summarize_raw_dumps_on_llm_failure(self, consolidator, mock_provider, store):
|
||
|
|
"""On LLM failure, raw-dump messages to HISTORY.md."""
|
||
|
|
mock_provider.chat_with_retry.side_effect = Exception("API error")
|
||
|
|
messages = [{"role": "user", "content": "hello"}]
|
||
|
|
result = await consolidator.archive(messages)
|
||
|
|
assert result is True # always succeeds
|
||
|
|
entries = store.read_unprocessed_history(since_cursor=0)
|
||
|
|
assert len(entries) == 1
|
||
|
|
assert "[RAW]" in entries[0]["content"]
|
||
|
|
|
||
|
|
async def test_summarize_skips_empty_messages(self, consolidator):
|
||
|
|
result = await consolidator.archive([])
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
|
||
|
|
class TestConsolidatorTokenBudget:
|
||
|
|
async def test_prompt_below_threshold_does_not_consolidate(self, consolidator):
|
||
|
|
"""No consolidation when tokens are within budget."""
|
||
|
|
session = MagicMock()
|
||
|
|
session.last_consolidated = 0
|
||
|
|
session.messages = [{"role": "user", "content": "hi"}]
|
||
|
|
session.key = "test:key"
|
||
|
|
consolidator.estimate_session_prompt_tokens = MagicMock(return_value=(100, "tiktoken"))
|
||
|
|
consolidator.archive = AsyncMock(return_value=True)
|
||
|
|
await consolidator.maybe_consolidate_by_tokens(session)
|
||
|
|
consolidator.archive.assert_not_called()
|