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:
Steve Yegge
2025-11-08 01:34:01 -08:00
parent e6d580e14b
commit 068615f28a
2 changed files with 257 additions and 4 deletions

View File

@@ -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__':