"""Integration tests for MemoryService assemble() replacement mode.
Tests that assemble returns messages[] without systemPromptAddition,
uses token budget management, and supports archive inclusion.
"""
import pytest
from unittest.mock import Mock, MagicMock, patch
from core.models import RequestContext
from server.memory_service import MemoryService
class TestAssembleReplacementMode:
"""Test MemoryService assemble() in pure message replacement mode."""
def test_assemble_returns_messages_not_system_prompt_addition(self):
"""Test that assemble returns messages[] without systemPromptAddition."""
service = MemoryService()
params = {
"messages": [
{"role": "user", "content": "What do you remember about my preferences?"}
],
"sessionId": "test-session",
}
with patch.object(service, 'get_read_api') as mock_get_read_api:
mock_api = Mock()
mock_result = Mock()
mock_result.hits = []
mock_api.search_memory.return_value = mock_result
mock_get_read_api.return_value = mock_api
result = service.compose(params)
assert "messages" in result
assert isinstance(result["messages"], list)
assert "systemPromptAddition" in result
assert "systemPromptSuffix" in result
assert "estimatedTokens" in result
assert "archiveCount" in result
assert "archiveIncluded" in result
assert len(result["messages"]) >= 1
assert result["messages"][-1]["role"] == "user"
def test_assemble_with_empty_query(self):
"""Test assemble with empty query returns original messages."""
service = MemoryService()
params = {
"messages": [{"role": "user", "content": ""}],
"sessionId": "test-session",
}
result = service.compose(params)
assert result["messages"] == params["messages"]
assert result["archiveCount"] == 0
assert result["archiveIncluded"] is False
assert result["estimatedTokens"] >= 0
def test_assemble_with_custom_token_budget(self):
"""Test assemble respects custom token budget."""
service = MemoryService()
params = {
"messages": [
{"role": "user", "content": "Tell me about my background"}
],
"tokenBudget": 50_000,
"sessionId": "test-session",
}
with patch.object(service, 'get_read_api') as mock_get_read_api:
mock_api = Mock()
mock_result = Mock()
mock_result.hits = []
mock_api.search_memory.return_value = mock_result
mock_get_read_api.return_value = mock_api
result = service.compose(params)
assert "messages" in result
assert "estimatedTokens" in result
assert result["estimatedTokens"] < 100_000
def test_assemble_includes_memory_hits_as_system_message(self):
"""Test that memory hits are injected as system message after original messages."""
service = MemoryService()
params = {
"messages": [
{"role": "user", "content": "What are my coding preferences?"}
],
"sessionId": "test-session",
}
with patch.object(service, 'get_read_api') as mock_get_read_api:
mock_api = Mock()
mock_result = Mock()
mock_hit = Mock()
mock_hit.category = "preference"
mock_hit.abstract = "Prefers Python for data science"
mock_hit.content_excerpt = ""
mock_hit.score = 0.85
mock_hit.relations = []
mock_result.hits = [mock_hit]
mock_api.search_memory.return_value = mock_result
mock_get_read_api.return_value = mock_api
result = service.compose(params)
assert result["systemPromptAddition"] == ""
assert result["memoryUserMessage"] == ""
msgs = result["messages"]
system_msgs = [m for m in msgs if m["role"] == "system"]
assert len(system_msgs) >= 1
ws_msg = [m for m in msgs if m["role"] == "system" and "Working Set" in m.get("content", "")]
assert len(ws_msg) >= 1
assert "preference" in ws_msg[0]["content"]
assert "Prefers Python" in ws_msg[0]["content"]
def test_assemble_graceful_degradation_on_error(self):
"""Test graceful degradation when memory search fails."""
service = MemoryService()
params = {
"messages": [
{"role": "user", "content": "Test query"}
],
"sessionId": "test-session",
}
with patch.object(service, 'get_read_api') as mock_get_read_api:
mock_get_read_api.side_effect = Exception("Search failed")
result = service.compose(params)
assert "messages" in result
assert len(result["messages"]) >= 1
assert result["systemPromptAddition"] == ""
assert result["memoryUserMessage"] == ""
assert result["archiveCount"] == 0
assert result["archiveIncluded"] is False
def test_assemble_archive_inclusion_placeholder(self):
"""Test that archive inclusion structure is in place (P1 stub)."""
service = MemoryService()
params = {
"messages": [
{"role": "user", "content": "What did we discuss yesterday?"}
],
"sessionId": "test-session",
}
with patch.object(service, 'get_read_api') as mock_get_read_api:
mock_api = Mock()
mock_result = Mock()
mock_result.hits = []
mock_api.search_memory.return_value = mock_result
mock_get_read_api.return_value = mock_api
result = service.compose(params)
assert "archiveCount" in result
assert "archiveIncluded" in result
assert result["archiveCount"] == 0
assert result["archiveIncluded"] is False
def test_format_memory_as_messages_helper(self):
"""Test _format_working_set helper (Layer 3)."""
from core.models import TokenBudget
service = MemoryService()
working_set = [{
"category": "preference",
"abstract": "Prefers dark mode",
"score": 0.9,
}]
msg = service._format_working_set(working_set, TokenBudget().allocate())
assert "Working Set" in msg
assert "preference" in msg
assert "dark mode" in msg
def test_format_memory_as_messages_empty(self):
"""Test _format_working_set with empty working set."""
service = MemoryService()
from core.models import TokenBudget
msg = service._format_working_set([], TokenBudget().allocate())
assert msg == ""
def test_estimate_tokens_helper(self):
"""Test _estimate_tokens helper."""
service = MemoryService()
messages = [
{"role": "user", "content": "Hello world " * 20},
{"role": "assistant", "content": "Hi there " * 30},
]
tokens = service._estimate_tokens(messages)
assert 100 <= tokens <= 200
def test_estimate_tokens_empty(self):
"""Test _estimate_tokens with empty messages."""
service = MemoryService()
tokens = service._estimate_tokens([])
assert tokens == 0
def test_collect_archives_stub(self):
"""Test _collect_archives returns empty lists (P1 stub)."""
service = MemoryService()
ctx = RequestContext(
account_id="test-account",
user_id="test-user",
agent_id="test-agent",
session_id="test-session",
trace_id="test-trace",
)
from core.models import TokenBudget
latest, pre = service._collect_archives(ctx, TokenBudget())
assert latest == []
assert pre == []
def test_assemble_preserves_original_messages(self):
"""Test that original messages are preserved in output."""
service = MemoryService()
original_messages = [
{"role": "user", "content": "First message"},
{"role": "assistant", "content": "First response"},
{"role": "user", "content": "Second message"},
]
params = {
"messages": original_messages,
"sessionId": "test-session",
}
with patch.object(service, 'get_read_api') as mock_get_read_api:
mock_api = Mock()
mock_result = Mock()
mock_result.hits = []
mock_api.search_memory.return_value = mock_result
mock_get_read_api.return_value = mock_api
result = service.compose(params)
result_messages = result["messages"]
contents = [m["content"] for m in result_messages]
for orig in original_messages:
assert orig["content"] in contents