diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e9aaec2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,56 @@ +""" +Shared test fixtures and config. + +Fixtures defined here available to all tests +""" + +from statistics import mode +from unittest.mock import AsyncMock, MagicMock + +import pytest +from anthropic.types import ContentBlock, Message, TextBlock, Usage + +from agent.config import Settings + + +@pytest.fixture +def settings(): + """provide test settings don't load from .env""" + return Settings( + anthropic_api_key="test-key-12345", + model="claude-test-model", + max_tokens=100, + safedir="./test-workspace", + ) + + +@pytest.fixture +def mock_anthropic_client(): + """Mock anthropic client that returns a fake response.""" + mock_client = AsyncMock() + + # create a realistic fake response + fake_message = Message( + id="msg_test123", + type="message", + role="assistant", + content=[TextBlock(type="text", text="42")], + model="claude-test-model", + stop_reason="end_turn", + usage=Usage(input_tokens=10, output_tokens=5), + ) + + # make messages.create() return this fake message + mock_client.messages.create = AsyncMock(return_value=fake_message) + + return mock_client + + +@pytest.fixture +def sample_history(): + """sample conversation history for testing""" + return [ + {"role": "user", "content": "Hello"}, + {"role": "assistance", "content": "Hi there!"}, + {"role": "user", "content": "What's 2+2?"}, + ] diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..d706156 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,49 @@ +"""test config""" + +import pytest +from pydantic import ValidationError + +from agent.config import Settings + + +def test_settings_with_all_values(): + """Test Settings loads correctly with all values provided.""" + settings = Settings( + anthropic_api_key="sk-ant-test", + model="claude-test", + max_tokens=500, + safedir="/tmp/test", + ) + + assert settings.anthropic_api_key == "sk-ant-test" + assert settings.model == "claude-test" + assert settings.max_tokens == 500 + + +def test_settings_defaults(): + """Test Settings uses defaults for optional values.""" + settings = Settings( + anthropic_api_key="sk-ant-test" # Only required field + ) + + # Should use defaults + assert settings.model == "claude-sonnet-4-5-20250929" + assert settings.max_tokens == 500 + + +def test_settings_missing_required_field(): + """Test Settings raises error when required field is missing.""" + with pytest.raises(ValidationError) as exc_info: + Settings(_env_file=None) # Missing anthropic_api_key + + # Verify the error mentions the missing field + assert "anthropic_api_key" in str(exc_info.value) + + +def test_settings_type_validation(): + """Test Settings validates types correctly.""" + with pytest.raises(ValidationError): + Settings( + anthropic_api_key="sk-ant-test", + max_tokens="not-a-number", # Should be int + ) diff --git a/tests/test_loop.py b/tests/test_loop.py new file mode 100644 index 0000000..b3570c0 --- /dev/null +++ b/tests/test_loop.py @@ -0,0 +1,38 @@ +from unittest.mock import AsyncMock, patch + +import pytest + +from agent.loop import run_turn + + +@pytest.mark.asyncio +async def test_run_turn_basic(mock_anthropic_client): + """test that run_turn calls the API and returns a message""" + + # patch the client with our mock + with patch("agent.loop.client", mock_anthropic_client): + result = await run_turn("What is 2+2?") + + # verify client was called + mock_anthropic_client.messages.create.assert_called_once() + + # verify message returned + assert result.content[0].text == "42" + + # verify call has correct parameters + call_args = mock_anthropic_client.messages.create.call_args + assert call_args.kwargs["messages"][0]["content"] == "What is 2+2?" + + +@pytest.mark.asyncio +async def test_run_turn_uses_settings(mock_anthropic_client, settings): + """Test that run_turn uses settings correctly.""" + + with patch("agent.loop.client", mock_anthropic_client): + with patch("agent.loop.settings", settings): + await run_turn("test message") + + # Verify settings were used + call_args = mock_anthropic_client.messages.create.call_args + assert call_args.kwargs["model"] == settings.model + assert call_args.kwargs["max_tokens"] == settings.max_tokens