Files
letta-server/letta/services/webhook_service_test.py
Shubham Naik 53d2bd0443 feat: support webhooks for step completions (#5904)
* chore: have letta fire a request after every step

* chore: have letta fire a request after every step

* chore: temporal

* chore: temporal

---------

Co-authored-by: Shubham Naik <shub@memgpt.ai>
2025-11-13 15:36:50 -08:00

120 lines
4.2 KiB
Python

"""
Simple test to verify webhook service functionality.
To run this test:
python -m pytest letta/services/webhook_service_test.py -v
To test with actual webhook:
export STEP_COMPLETE_WEBHOOK=https://your-webhook-url.com/endpoint
export STEP_COMPLETE_KEY=your-secret-key
python -m pytest letta/services/webhook_service_test.py -v
These tests verify the webhook service works in both:
- Temporal mode (when webhooks are called as Temporal activities)
- Non-Temporal mode (when webhooks are called directly from StepManager)
"""
import os
from unittest.mock import AsyncMock, patch
import pytest
from letta.services.webhook_service import WebhookService
@pytest.mark.asyncio
async def test_webhook_not_configured():
"""Test that webhook does not send when URL is not configured."""
with patch.dict(os.environ, {}, clear=True):
service = WebhookService()
result = await service.notify_step_complete("step_123")
assert result is False
@pytest.mark.asyncio
async def test_webhook_success():
"""Test successful webhook notification."""
with patch.dict(
os.environ,
{"STEP_COMPLETE_WEBHOOK": "https://example.com/webhook", "STEP_COMPLETE_KEY": "test-key"},
):
service = WebhookService()
with patch("httpx.AsyncClient") as mock_client:
mock_response = AsyncMock()
mock_response.status_code = 200
mock_response.raise_for_status = AsyncMock()
mock_post = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value.post = mock_post
result = await service.notify_step_complete("step_123")
assert result is True
mock_post.assert_called_once()
call_args = mock_post.call_args
assert call_args.kwargs["json"] == {"step_id": "step_123"}
assert call_args.kwargs["headers"]["Authorization"] == "Bearer test-key"
@pytest.mark.asyncio
async def test_webhook_without_auth():
"""Test webhook notification without authentication key."""
with patch.dict(os.environ, {"STEP_COMPLETE_WEBHOOK": "https://example.com/webhook"}, clear=True):
service = WebhookService()
with patch("httpx.AsyncClient") as mock_client:
mock_response = AsyncMock()
mock_response.status_code = 200
mock_response.raise_for_status = AsyncMock()
mock_post = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value.post = mock_post
result = await service.notify_step_complete("step_123")
assert result is True
call_args = mock_post.call_args
# Should not have Authorization header
assert "Authorization" not in call_args.kwargs["headers"]
@pytest.mark.asyncio
async def test_webhook_timeout():
"""Test webhook notification timeout handling."""
with patch.dict(os.environ, {"STEP_COMPLETE_WEBHOOK": "https://example.com/webhook"}):
service = WebhookService()
with patch("httpx.AsyncClient") as mock_client:
import httpx
mock_post = AsyncMock(side_effect=httpx.TimeoutException("Request timed out"))
mock_client.return_value.__aenter__.return_value.post = mock_post
result = await service.notify_step_complete("step_123")
assert result is False
@pytest.mark.asyncio
async def test_webhook_http_error():
"""Test webhook notification HTTP error handling."""
with patch.dict(os.environ, {"STEP_COMPLETE_WEBHOOK": "https://example.com/webhook"}):
service = WebhookService()
with patch("httpx.AsyncClient") as mock_client:
import httpx
mock_response = AsyncMock()
mock_response.status_code = 500
mock_response.raise_for_status = AsyncMock(
side_effect=httpx.HTTPStatusError("Server error", request=None, response=mock_response)
)
mock_post = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value.post = mock_post
result = await service.notify_step_complete("step_123")
assert result is False