- Add save-only endpoint (POST /conversations/{id}/save) for persisting
messages without triggering agent execution
- Add sessions command to list previous conversation sessions
- Add --resume flag to chat command for resuming sessions by ID
- Buffer streamed responses and save after completion
- Update AGENTS.md with session commands and remove outdated limitation
- Add 3 new tests for save endpoint (208 total)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
384 lines
13 KiB
Python
384 lines
13 KiB
Python
"""
|
|
Tests for conversations domain.
|
|
|
|
Tests conversation CRUD, context building, and API endpoints.
|
|
"""
|
|
import pytest
|
|
from uuid import uuid4
|
|
|
|
from src.domains.conversations.models import Conversation, Message
|
|
from src.domains.conversations.schemas import (
|
|
CreateConversationRequest,
|
|
AddMessageRequest,
|
|
ConversationResponse,
|
|
MessageResponse,
|
|
)
|
|
|
|
|
|
class TestConversationModels:
|
|
"""Tests for conversation database models."""
|
|
|
|
def test_conversation_creation(self):
|
|
"""Test Conversation model creation with explicit values."""
|
|
conv = Conversation(
|
|
user_id="test-user",
|
|
agent_type="explore",
|
|
working_dir=".",
|
|
total_tokens=0,
|
|
)
|
|
assert conv.user_id == "test-user"
|
|
assert conv.agent_type == "explore"
|
|
assert conv.working_dir == "."
|
|
assert conv.total_tokens == 0
|
|
|
|
def test_conversation_with_values(self):
|
|
"""Test Conversation with explicit values."""
|
|
conv = Conversation(
|
|
user_id="test-user",
|
|
agent_type="plan",
|
|
working_dir="/tmp/project",
|
|
title="Test Conversation",
|
|
)
|
|
assert conv.agent_type == "plan"
|
|
assert conv.working_dir == "/tmp/project"
|
|
assert conv.title == "Test Conversation"
|
|
|
|
def test_message_creation(self):
|
|
"""Test Message model creation with explicit values."""
|
|
msg = Message(
|
|
conversation_id=uuid4(),
|
|
role="user",
|
|
content="Hello",
|
|
token_count=0,
|
|
is_summary=False,
|
|
)
|
|
assert msg.role == "user"
|
|
assert msg.content == "Hello"
|
|
assert msg.token_count == 0
|
|
assert msg.is_summary is False
|
|
|
|
def test_message_repr(self):
|
|
"""Test Message string representation."""
|
|
msg = Message(
|
|
conversation_id=uuid4(),
|
|
role="user",
|
|
content="This is a test message",
|
|
)
|
|
repr_str = repr(msg)
|
|
assert "user" in repr_str
|
|
assert "This is a test" in repr_str
|
|
|
|
|
|
class TestConversationSchemas:
|
|
"""Tests for Pydantic schemas."""
|
|
|
|
def test_create_request_defaults(self):
|
|
"""Test CreateConversationRequest defaults."""
|
|
request = CreateConversationRequest()
|
|
assert request.agent_type == "explore"
|
|
assert request.working_dir == "."
|
|
assert request.title is None
|
|
|
|
def test_create_request_custom(self):
|
|
"""Test CreateConversationRequest with values."""
|
|
request = CreateConversationRequest(
|
|
agent_type="task",
|
|
working_dir="/home/user/project",
|
|
title="My Task",
|
|
)
|
|
assert request.agent_type == "task"
|
|
assert request.working_dir == "/home/user/project"
|
|
assert request.title == "My Task"
|
|
|
|
def test_add_message_request_valid(self):
|
|
"""Test AddMessageRequest validation."""
|
|
request = AddMessageRequest(content="Hello, world!")
|
|
assert request.content == "Hello, world!"
|
|
|
|
def test_add_message_request_empty_fails(self):
|
|
"""Test that empty content fails validation."""
|
|
with pytest.raises(ValueError):
|
|
AddMessageRequest(content="")
|
|
|
|
|
|
class TestConversationAPI:
|
|
"""Tests for conversation API endpoints."""
|
|
|
|
@pytest.mark.anyio
|
|
async def test_create_conversation(self, auth_client):
|
|
"""Test creating a conversation."""
|
|
response = await auth_client.post(
|
|
"/conversations/",
|
|
json={"agent_type": "explore", "working_dir": "."}
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert "id" in data
|
|
assert data["agent_type"] == "explore"
|
|
assert data["total_tokens"] == 0
|
|
|
|
@pytest.mark.anyio
|
|
async def test_create_conversation_with_title(self, auth_client):
|
|
"""Test creating a conversation with title."""
|
|
response = await auth_client.post(
|
|
"/conversations/",
|
|
json={
|
|
"agent_type": "plan",
|
|
"working_dir": "/tmp",
|
|
"title": "Planning Session"
|
|
}
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["title"] == "Planning Session"
|
|
assert data["agent_type"] == "plan"
|
|
|
|
@pytest.mark.anyio
|
|
async def test_list_conversations_empty(self, auth_client):
|
|
"""Test listing conversations when empty."""
|
|
response = await auth_client.get("/conversations/")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "conversations" in data
|
|
assert "total" in data
|
|
|
|
@pytest.mark.anyio
|
|
async def test_get_conversation_not_found(self, auth_client):
|
|
"""Test getting non-existent conversation."""
|
|
fake_id = uuid4()
|
|
response = await auth_client.get(f"/conversations/{fake_id}")
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.anyio
|
|
async def test_delete_conversation_not_found(self, auth_client):
|
|
"""Test deleting non-existent conversation."""
|
|
fake_id = uuid4()
|
|
response = await auth_client.delete(f"/conversations/{fake_id}")
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.anyio
|
|
async def test_add_message_not_found(self, auth_client):
|
|
"""Test adding message to non-existent conversation."""
|
|
fake_id = uuid4()
|
|
response = await auth_client.post(
|
|
f"/conversations/{fake_id}/messages",
|
|
json={"content": "Hello"}
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestConversationService:
|
|
"""Tests for ConversationService business logic."""
|
|
|
|
@pytest.mark.anyio
|
|
async def test_context_prompt_no_history(self):
|
|
"""Test building context prompt with no history."""
|
|
from src.domains.conversations.service import ConversationService
|
|
from unittest.mock import MagicMock
|
|
|
|
# Create mock session
|
|
mock_session = MagicMock()
|
|
service = ConversationService(mock_session)
|
|
|
|
prompt = service.build_context_prompt([], "What files are here?")
|
|
|
|
assert "<current_request>" in prompt
|
|
assert "What files are here?" in prompt
|
|
assert "<recent_conversation>" not in prompt
|
|
assert "<conversation_summary>" not in prompt
|
|
|
|
@pytest.mark.anyio
|
|
async def test_context_prompt_with_history(self):
|
|
"""Test building context prompt with message history."""
|
|
from src.domains.conversations.service import ConversationService
|
|
from src.domains.conversations.models import Message
|
|
from unittest.mock import MagicMock
|
|
|
|
mock_session = MagicMock()
|
|
service = ConversationService(mock_session)
|
|
|
|
messages = [
|
|
Message(
|
|
conversation_id=uuid4(),
|
|
role="user",
|
|
content="Find Python files",
|
|
),
|
|
Message(
|
|
conversation_id=uuid4(),
|
|
role="assistant",
|
|
content="Found 10 Python files.",
|
|
),
|
|
]
|
|
|
|
prompt = service.build_context_prompt(messages, "Show the largest")
|
|
|
|
assert "<recent_conversation>" in prompt
|
|
assert "USER: Find Python files" in prompt
|
|
assert "ASSISTANT: Found 10 Python files" in prompt
|
|
assert "<current_request>" in prompt
|
|
assert "Show the largest" in prompt
|
|
|
|
@pytest.mark.anyio
|
|
async def test_context_prompt_with_summary(self):
|
|
"""Test building context prompt with summary message."""
|
|
from src.domains.conversations.service import ConversationService
|
|
from src.domains.conversations.models import Message
|
|
from unittest.mock import MagicMock
|
|
|
|
mock_session = MagicMock()
|
|
service = ConversationService(mock_session)
|
|
|
|
messages = [
|
|
Message(
|
|
conversation_id=uuid4(),
|
|
role="summary",
|
|
content="Previously discussed: project setup",
|
|
is_summary=True,
|
|
),
|
|
Message(
|
|
conversation_id=uuid4(),
|
|
role="user",
|
|
content="Now what?",
|
|
),
|
|
]
|
|
|
|
prompt = service.build_context_prompt(messages, "Continue")
|
|
|
|
assert "<conversation_summary>" in prompt
|
|
assert "Previously discussed: project setup" in prompt
|
|
|
|
|
|
class TestSummarization:
|
|
"""Tests for conversation summarization."""
|
|
|
|
def test_format_messages_for_summary(self):
|
|
"""Test formatting messages for summarization."""
|
|
from src.domains.conversations.summarize import format_messages_for_summary
|
|
from src.domains.conversations.models import Message
|
|
|
|
messages = [
|
|
Message(
|
|
conversation_id=uuid4(),
|
|
role="user",
|
|
content="Hello",
|
|
),
|
|
Message(
|
|
conversation_id=uuid4(),
|
|
role="assistant",
|
|
content="Hi there!",
|
|
),
|
|
]
|
|
|
|
formatted = format_messages_for_summary(messages)
|
|
|
|
assert "USER: Hello" in formatted
|
|
assert "ASSISTANT: Hi there!" in formatted
|
|
|
|
def test_format_messages_with_summary(self):
|
|
"""Test formatting messages that include a summary."""
|
|
from src.domains.conversations.summarize import format_messages_for_summary
|
|
from src.domains.conversations.models import Message
|
|
|
|
messages = [
|
|
Message(
|
|
conversation_id=uuid4(),
|
|
role="summary",
|
|
content="Previous context summary",
|
|
is_summary=True,
|
|
),
|
|
Message(
|
|
conversation_id=uuid4(),
|
|
role="user",
|
|
content="Continue",
|
|
),
|
|
]
|
|
|
|
formatted = format_messages_for_summary(messages)
|
|
|
|
assert "[Previous Summary]" in formatted
|
|
assert "Previous context summary" in formatted
|
|
|
|
|
|
class TestSaveMessagesAPI:
|
|
"""Tests for the save messages endpoint (no agent execution)."""
|
|
|
|
@pytest.mark.anyio
|
|
async def test_save_messages(self, auth_client):
|
|
"""Test saving a message pair without triggering agent."""
|
|
# First create a conversation
|
|
response = await auth_client.post(
|
|
"/conversations/",
|
|
json={"agent_type": "task", "working_dir": "."}
|
|
)
|
|
assert response.status_code == 201
|
|
conv_id = response.json()["id"]
|
|
|
|
# Save a message pair
|
|
response = await auth_client.post(
|
|
f"/conversations/{conv_id}/save",
|
|
json={
|
|
"user_content": "Find all Python files",
|
|
"assistant_content": "I found 5 Python files in the project.",
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert data["user_message"]["role"] == "user"
|
|
assert data["user_message"]["content"] == "Find all Python files"
|
|
assert data["assistant_message"]["role"] == "assistant"
|
|
assert data["assistant_message"]["content"] == "I found 5 Python files in the project."
|
|
assert data["total_tokens"] > 0
|
|
|
|
@pytest.mark.anyio
|
|
async def test_save_messages_not_found(self, auth_client):
|
|
"""Test saving messages to non-existent conversation."""
|
|
fake_id = uuid4()
|
|
response = await auth_client.post(
|
|
f"/conversations/{fake_id}/save",
|
|
json={
|
|
"user_content": "Test",
|
|
"assistant_content": "Response",
|
|
}
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.anyio
|
|
async def test_save_messages_updates_token_count(self, auth_client):
|
|
"""Test that saving messages updates the conversation token count."""
|
|
# Create conversation
|
|
response = await auth_client.post(
|
|
"/conversations/",
|
|
json={"agent_type": "explore", "working_dir": "."}
|
|
)
|
|
conv_id = response.json()["id"]
|
|
assert response.json()["total_tokens"] == 0
|
|
|
|
# Save first message pair
|
|
response = await auth_client.post(
|
|
f"/conversations/{conv_id}/save",
|
|
json={
|
|
"user_content": "Hello",
|
|
"assistant_content": "Hi there!",
|
|
}
|
|
)
|
|
first_tokens = response.json()["total_tokens"]
|
|
assert first_tokens > 0
|
|
|
|
# Save second message pair
|
|
response = await auth_client.post(
|
|
f"/conversations/{conv_id}/save",
|
|
json={
|
|
"user_content": "How are you?",
|
|
"assistant_content": "I'm doing well, thank you for asking!",
|
|
}
|
|
)
|
|
second_tokens = response.json()["total_tokens"]
|
|
assert second_tokens > first_tokens
|
|
|
|
# Verify via get endpoint
|
|
response = await auth_client.get(f"/conversations/{conv_id}")
|
|
assert response.status_code == 200
|
|
assert response.json()["total_tokens"] == second_tokens
|
|
assert len(response.json()["messages"]) == 4
|