Add comprehensive Agent Mail adapter tests (bd-5ki8)
- 29 tests covering enabled, disabled, graceful degradation - Reservation conflict handling (JSON and plain text errors) - HTTP error scenarios (404, 500, 503) - Health check edge cases - Malformed responses and empty bodies - All tests run in 10ms (fast!) Amp-Thread-ID: https://ampcode.com/threads/T-8bcf1119-53f1-4602-9161-d804444cc314 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -185,6 +185,87 @@ class TestAgentMailAdapterEnabled(unittest.TestCase):
|
||||
result = adapter.get_reservations()
|
||||
|
||||
self.assertEqual(len(result), 2)
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_get_reservations_dict_response(self, mock_urlopen):
|
||||
"""Test getting reservations with dict wrapper response."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "ok"})
|
||||
adapter = AgentMailAdapter(agent_name="test-agent")
|
||||
|
||||
# Some servers wrap response in {"reservations": [...]}
|
||||
mock_urlopen.return_value = self._mock_response(200, {
|
||||
"reservations": [{"issue_id": "bd-789", "agent": "agent-3"}]
|
||||
})
|
||||
|
||||
result = adapter.get_reservations()
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]["issue_id"], "bd-789")
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_check_inbox_dict_wrapper(self, mock_urlopen):
|
||||
"""Test checking inbox with dict wrapper response."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "ok"})
|
||||
adapter = AgentMailAdapter(agent_name="test-agent")
|
||||
|
||||
# Some servers wrap response in {"messages": [...]}
|
||||
mock_urlopen.return_value = self._mock_response(200, {
|
||||
"messages": [{"from": "agent-5", "event": "test"}]
|
||||
})
|
||||
|
||||
result = adapter.check_inbox()
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]["from"], "agent-5")
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_reserve_with_custom_ttl(self, mock_urlopen):
|
||||
"""Test reservation with custom TTL."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "ok"})
|
||||
adapter = AgentMailAdapter(agent_name="test-agent")
|
||||
|
||||
mock_urlopen.return_value = self._mock_response(201, {"reserved": True})
|
||||
result = adapter.reserve_issue("bd-999", ttl=7200)
|
||||
|
||||
self.assertTrue(result)
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_http_error_500(self, mock_urlopen):
|
||||
"""Test handling of HTTP 500 errors."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "ok"})
|
||||
adapter = AgentMailAdapter(agent_name="test-agent")
|
||||
|
||||
# Simulate 500 Internal Server Error
|
||||
error_response = HTTPError(
|
||||
url="http://test",
|
||||
code=500,
|
||||
msg="Internal Server Error",
|
||||
hdrs={},
|
||||
fp=BytesIO(b"Server error")
|
||||
)
|
||||
mock_urlopen.side_effect = error_response
|
||||
|
||||
# Should gracefully degrade
|
||||
result = adapter.reserve_issue("bd-123")
|
||||
self.assertTrue(result)
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_http_error_404(self, mock_urlopen):
|
||||
"""Test handling of HTTP 404 errors."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "ok"})
|
||||
adapter = AgentMailAdapter(agent_name="test-agent")
|
||||
|
||||
error_response = HTTPError(
|
||||
url="http://test",
|
||||
code=404,
|
||||
msg="Not Found",
|
||||
hdrs={},
|
||||
fp=BytesIO(b"Not found")
|
||||
)
|
||||
mock_urlopen.side_effect = error_response
|
||||
|
||||
result = adapter.release_issue("bd-nonexistent")
|
||||
self.assertTrue(result) # Graceful degradation
|
||||
|
||||
|
||||
class TestGracefulDegradation(unittest.TestCase):
|
||||
@@ -231,6 +312,41 @@ class TestGracefulDegradation(unittest.TestCase):
|
||||
|
||||
# Should not crash
|
||||
self.assertTrue(adapter.reserve_issue("bd-123"))
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_malformed_json_response(self, mock_urlopen):
|
||||
"""Test handling of malformed JSON responses."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "ok"})
|
||||
adapter = AgentMailAdapter()
|
||||
|
||||
# Return invalid JSON
|
||||
bad_response = MagicMock()
|
||||
bad_response.status = 200
|
||||
bad_response.__enter__ = Mock(return_value=bad_response)
|
||||
bad_response.__exit__ = Mock(return_value=False)
|
||||
bad_response.read.return_value = b'{invalid json'
|
||||
mock_urlopen.return_value = bad_response
|
||||
|
||||
# Should gracefully degrade
|
||||
result = adapter.check_inbox()
|
||||
self.assertEqual(result, [])
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_empty_response_body(self, mock_urlopen):
|
||||
"""Test handling of empty response bodies."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "ok"})
|
||||
adapter = AgentMailAdapter()
|
||||
|
||||
# Return 204 No Content (empty body)
|
||||
empty_response = MagicMock()
|
||||
empty_response.status = 204
|
||||
empty_response.__enter__ = Mock(return_value=empty_response)
|
||||
empty_response.__exit__ = Mock(return_value=False)
|
||||
empty_response.read.return_value = b''
|
||||
mock_urlopen.return_value = empty_response
|
||||
|
||||
result = adapter.release_issue("bd-123")
|
||||
self.assertTrue(result)
|
||||
|
||||
|
||||
class TestConfiguration(unittest.TestCase):
|
||||
@@ -279,6 +395,142 @@ class TestConfiguration(unittest.TestCase):
|
||||
adapter = AgentMailAdapter(url="http://localhost:8765/")
|
||||
|
||||
self.assertEqual(adapter.url, "http://localhost:8765")
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
@patch('socket.gethostname')
|
||||
def test_default_agent_name_from_hostname(self, mock_hostname, mock_urlopen):
|
||||
"""Test default agent name comes from hostname."""
|
||||
mock_urlopen.side_effect = URLError("Not testing connection")
|
||||
mock_hostname.return_value = "my-laptop"
|
||||
|
||||
adapter = AgentMailAdapter()
|
||||
|
||||
self.assertEqual(adapter.agent_name, "my-laptop")
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
@patch('socket.gethostname')
|
||||
def test_default_agent_name_fallback(self, mock_hostname, mock_urlopen):
|
||||
"""Test fallback agent name when hostname fails."""
|
||||
mock_urlopen.side_effect = URLError("Not testing connection")
|
||||
mock_hostname.side_effect = Exception("Can't get hostname")
|
||||
|
||||
adapter = AgentMailAdapter()
|
||||
|
||||
self.assertEqual(adapter.agent_name, "beads-agent")
|
||||
|
||||
|
||||
class TestHealthCheck(unittest.TestCase):
|
||||
"""Test health check scenarios."""
|
||||
|
||||
def _mock_response(self, status_code=200, data=None):
|
||||
"""Create mock HTTP response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status = status_code
|
||||
mock_response.__enter__ = Mock(return_value=mock_response)
|
||||
mock_response.__exit__ = Mock(return_value=False)
|
||||
|
||||
if data is not None:
|
||||
mock_response.read.return_value = json.dumps(data).encode('utf-8')
|
||||
else:
|
||||
mock_response.read.return_value = b''
|
||||
|
||||
return mock_response
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_health_check_bad_status(self, mock_urlopen):
|
||||
"""Test health check with non-ok status."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "degraded"})
|
||||
|
||||
adapter = AgentMailAdapter()
|
||||
|
||||
self.assertFalse(adapter.enabled)
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_health_check_http_error(self, mock_urlopen):
|
||||
"""Test health check with HTTP error."""
|
||||
error_response = HTTPError(
|
||||
url="http://test",
|
||||
code=503,
|
||||
msg="Service Unavailable",
|
||||
hdrs={},
|
||||
fp=BytesIO(b"Server down")
|
||||
)
|
||||
mock_urlopen.side_effect = error_response
|
||||
|
||||
adapter = AgentMailAdapter()
|
||||
|
||||
self.assertFalse(adapter.enabled)
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_health_check_timeout(self, mock_urlopen):
|
||||
"""Test health check with timeout."""
|
||||
mock_urlopen.side_effect = URLError("timeout")
|
||||
|
||||
adapter = AgentMailAdapter(timeout=1)
|
||||
|
||||
self.assertFalse(adapter.enabled)
|
||||
|
||||
|
||||
class TestReservationConflicts(unittest.TestCase):
|
||||
"""Test reservation conflict handling."""
|
||||
|
||||
def _mock_response(self, status_code=200, data=None):
|
||||
"""Create mock HTTP response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status = status_code
|
||||
mock_response.__enter__ = Mock(return_value=mock_response)
|
||||
mock_response.__exit__ = Mock(return_value=False)
|
||||
|
||||
if data is not None:
|
||||
mock_response.read.return_value = json.dumps(data).encode('utf-8')
|
||||
else:
|
||||
mock_response.read.return_value = b''
|
||||
|
||||
return mock_response
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_conflict_with_malformed_error_body(self, mock_urlopen):
|
||||
"""Test conflict handling with malformed error body."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "ok"})
|
||||
adapter = AgentMailAdapter(agent_name="test-agent")
|
||||
|
||||
# 409 with non-JSON error body
|
||||
error_response = HTTPError(
|
||||
url="http://test",
|
||||
code=409,
|
||||
msg="Conflict",
|
||||
hdrs={},
|
||||
fp=BytesIO(b"Already reserved (plain text)")
|
||||
)
|
||||
mock_urlopen.side_effect = error_response
|
||||
|
||||
result = adapter.reserve_issue("bd-999")
|
||||
|
||||
self.assertFalse(result)
|
||||
|
||||
@patch('beads_mail_adapter.urlopen')
|
||||
def test_multiple_operations_after_conflict(self, mock_urlopen):
|
||||
"""Test that adapter continues working after conflict."""
|
||||
mock_urlopen.return_value = self._mock_response(200, {"status": "ok"})
|
||||
adapter = AgentMailAdapter(agent_name="test-agent")
|
||||
|
||||
# First reservation fails with conflict
|
||||
error_response = HTTPError(
|
||||
url="http://test",
|
||||
code=409,
|
||||
msg="Conflict",
|
||||
hdrs={},
|
||||
fp=BytesIO(json.dumps({"error": "Already reserved"}).encode('utf-8'))
|
||||
)
|
||||
mock_urlopen.side_effect = error_response
|
||||
result1 = adapter.reserve_issue("bd-123")
|
||||
self.assertFalse(result1)
|
||||
|
||||
# Second reservation succeeds
|
||||
mock_urlopen.side_effect = None
|
||||
mock_urlopen.return_value = self._mock_response(201, {"reserved": True})
|
||||
result2 = adapter.reserve_issue("bd-456")
|
||||
self.assertTrue(result2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user