File size: 9,536 Bytes
0497d92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ca285d
 
0497d92
 
 
 
 
8ca285d
 
0497d92
 
 
 
 
 
 
 
 
8ca285d
0497d92
 
8ca285d
 
 
0497d92
 
 
8ca285d
0497d92
 
 
 
 
 
 
 
 
 
8ca285d
0497d92
 
8ca285d
0497d92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ca285d
 
0497d92
 
 
 
 
 
 
 
 
 
 
 
 
 
8ca285d
0497d92
 
8ca285d
0497d92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ca285d
 
0497d92
 
 
 
 
 
8ca285d
0497d92
8ca285d
0497d92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ca285d
0497d92
 
 
 
 
 
 
8ca285d
 
 
 
 
 
 
0497d92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""
Tests specifically for 502 Bad Gateway error prevention.
"""
import pytest
import httpx
from unittest.mock import patch, MagicMock
from starlette.testclient import TestClient
from app.main import app
from tests.test_services import StubAsyncClient, StubAsyncResponse


client = TestClient(app)


class Test502BadGatewayPrevention:
    """Test that 502 Bad Gateway errors are prevented and handled properly."""

    @pytest.mark.integration
    def test_no_502_for_timeout_errors(self):
        """Test that timeout errors return 504 instead of 502."""
        with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_exc=httpx.TimeoutException("Timeout"))):
            resp = client.post(
                "/api/v1/summarize/",
                json={"text": "Test text that will timeout"}
            )
            
            # Should return 504 Gateway Timeout, not 502 Bad Gateway
            assert resp.status_code == 504
            assert resp.status_code != 502
            
            data = resp.json()
            assert "timeout" in data["detail"].lower()
            assert "text may be too long" in data["detail"].lower()

    @pytest.mark.integration
    def test_large_text_gets_extended_timeout(self):
        """Test that large text gets extended timeout to prevent 502 errors."""
        large_text = "A" * 10000  # 10,000 characters
        
        with patch('httpx.AsyncClient') as mock_client:
            mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse())
            
            resp = client.post(
                "/api/v1/summarize/",
                json={"text": large_text, "max_tokens": 256}
            )
            
            # Verify extended timeout was used
            mock_client.assert_called_once()
            call_args = mock_client.call_args
            # Timeout calculated with ORIGINAL text length (10000 chars): 30 + (10000-1000)//1000*3 = 30 + 27 = 57
            expected_timeout = 30 + (10000 - 1000) // 1000 * 3  # 57 seconds
            assert call_args[1]['timeout'] == expected_timeout

    @pytest.mark.integration
    def test_very_large_text_gets_capped_timeout(self):
        """Test that very large text gets capped timeout to prevent infinite waits."""
        # Use 32000 chars (max allowed) instead of 100000 (exceeds validation)
        very_large_text = "A" * 32000  # 32,000 characters (max allowed)
        
        with patch('httpx.AsyncClient') as mock_client:
            mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse())
            
            resp = client.post(
                "/api/v1/summarize/",
                json={"text": very_large_text, "max_tokens": 256}
            )
            
            # Verify timeout is capped at 90 seconds (actual cap)
            mock_client.assert_called_once()
            call_args = mock_client.call_args
            # Timeout calculated with ORIGINAL text length (32000 chars): 30 + (32000-1000)//1000*3 = 30 + 93 = 123, capped at 90
            expected_timeout = 90  # Capped at 90 seconds
            assert call_args[1]['timeout'] == expected_timeout

    @pytest.mark.integration
    def test_small_text_uses_base_timeout(self):
        """Test that small text uses base timeout (30 seconds in test env)."""
        small_text = "Short text"
        
        with patch('httpx.AsyncClient') as mock_client:
            mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse())
            
            resp = client.post(
                "/api/v1/summarize/",
                json={"text": small_text, "max_tokens": 256}
            )
            
            # Verify base timeout was used (test env uses 30s)
            mock_client.assert_called_once()
            call_args = mock_client.call_args
            assert call_args[1]['timeout'] == 30  # Base timeout in test env

    @pytest.mark.integration
    def test_medium_text_gets_appropriate_timeout(self):
        """Test that medium-sized text gets appropriate timeout."""
        medium_text = "A" * 5000  # 5,000 characters
        
        with patch('httpx.AsyncClient') as mock_client:
            mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse())
            
            resp = client.post(
                "/api/v1/summarize/",
                json={"text": medium_text, "max_tokens": 256}
            )
            
            # Verify appropriate timeout was used
            mock_client.assert_called_once()
            call_args = mock_client.call_args
            # Timeout calculated with ORIGINAL text length (5000 chars): 30 + (5000-1000)//1000*3 = 30 + 12 = 42
            expected_timeout = 30 + (5000 - 1000) // 1000 * 3  # 42 seconds
            assert call_args[1]['timeout'] == expected_timeout

    @pytest.mark.integration
    def test_timeout_error_has_helpful_message(self):
        """Test that timeout errors provide helpful guidance."""
        with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_exc=httpx.TimeoutException("Timeout"))):
            resp = client.post(
                "/api/v1/summarize/",
                json={"text": "Test text"}
            )
            
            assert resp.status_code == 504
            data = resp.json()
            
            # Check for helpful error message (actual message uses "reducing" not "reduce")
            assert "timeout" in data["detail"].lower()
            assert "text may be too long" in data["detail"].lower()
            assert "reducing" in data["detail"].lower()
            assert "max_tokens" in data["detail"].lower()

    @pytest.mark.integration
    def test_http_errors_still_return_502(self):
        """Test that actual HTTP errors still return 502 (this is correct behavior)."""
        http_error = httpx.HTTPStatusError("Bad Request", request=MagicMock(), response=MagicMock())
        
        with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_exc=http_error)):
            resp = client.post(
                "/api/v1/summarize/",
                json={"text": "Test text"}
            )
            
            # HTTP errors should still return 502
            assert resp.status_code == 502
            data = resp.json()
            assert "Summarization failed" in data["detail"]

    @pytest.mark.integration
    def test_unexpected_errors_return_502(self):
        """Test that unexpected errors return 502 Bad Gateway (actual behavior)."""
        with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_exc=Exception("Unexpected error"))):
            resp = client.post(
                "/api/v1/summarize/",
                json={"text": "Test text"}
            )
            
            assert resp.status_code == 502  # Actual behavior
            data = resp.json()
            assert "Summarization failed" in data["detail"]

    @pytest.mark.integration
    def test_successful_large_text_processing(self):
        """Test that large text can be processed successfully with extended timeout."""
        large_text = "A" * 5000  # 5,000 characters
        mock_response = {
            "response": "This is a summary of the large text.",
            "eval_count": 25,
            "done": True
        }
        
        with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_result=StubAsyncResponse(json_data=mock_response))):
            resp = client.post(
                "/api/v1/summarize/",
                json={"text": large_text, "max_tokens": 256}
            )
            
            # Should succeed with 200
            assert resp.status_code == 200
            data = resp.json()
            assert data["summary"] == mock_response["response"]
            assert data["model"] == "llama3.2:1b"
            assert data["tokens_used"] == mock_response["eval_count"]
            assert "latency_ms" in data

    @pytest.mark.integration
    def test_dynamic_timeout_calculation_formula(self):
        """Test the exact formula for dynamic timeout calculation."""
        test_cases = [
            (500, 30),      # Small text: base timeout (30s in test env)
            (1000, 30),     # Exactly 1000 chars: base timeout (30s)
            (1500, 30),     # 1500 chars: 30 + (500//1000)*3 = 30 + 0*3 = 30
            (2000, 33),      # 2000 chars: 30 + (1000//1000)*3 = 30 + 1*3 = 33
            (5000, 42),      # 5000 chars: 30 + (4000//1000)*3 = 30 + 4*3 = 42 (calculated with original length)
            (10000, 57),     # 10000 chars: 30 + (9000//1000)*3 = 30 + 9*3 = 57 (calculated with original length)
            (32000, 90),     # Max allowed: 30 + (31000//1000)*3 = 30 + 31*3 = 123, capped at 90
        ]
        
        for text_length, expected_timeout in test_cases:
            test_text = "A" * text_length
            
            with patch('httpx.AsyncClient') as mock_client:
                mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse())
                
                resp = client.post(
                    "/api/v1/summarize/",
                    json={"text": test_text, "max_tokens": 256}
                )
                
                # Verify timeout calculation
                mock_client.assert_called_once()
                call_args = mock_client.call_args
                actual_timeout = call_args[1]['timeout']
                assert actual_timeout == expected_timeout, f"Text length {text_length} should have timeout {expected_timeout}, got {actual_timeout}"