Files
webber/webber-api/tests/test_conversations.py
T
jpmschweitzerandClaude Opus 4.5 daa9543790 feat: add session persistence to CLI
- 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>
2026-01-14 09:50:25 +01:00

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