@cdklabs/cdk-appmod-catalog-blueprints 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsii +1276 -211
- package/lib/document-processing/adapter/queued-s3-adapter.js +1 -1
- package/lib/document-processing/agentic-document-processing.js +2 -2
- package/lib/document-processing/base-document-processing.js +1 -1
- package/lib/document-processing/bedrock-document-processing.js +1 -1
- package/lib/document-processing/default-document-processing-config.js +1 -1
- package/lib/framework/agents/base-agent.d.ts +15 -2
- package/lib/framework/agents/base-agent.js +3 -3
- package/lib/framework/agents/batch-agent.d.ts +1 -1
- package/lib/framework/agents/batch-agent.js +2 -2
- package/lib/framework/agents/default-agent-config.js +1 -1
- package/lib/framework/agents/interactive-agent.d.ts +286 -3
- package/lib/framework/agents/interactive-agent.js +412 -123
- package/lib/framework/agents/knowledge-base/base-knowledge-base.js +1 -1
- package/lib/framework/agents/knowledge-base/bedrock-knowledge-base.js +1 -1
- package/lib/framework/agents/resources/agentcore-agent-handler/Dockerfile +20 -0
- package/lib/framework/agents/resources/agentcore-agent-handler/main.py +224 -0
- package/lib/framework/agents/resources/agentcore-agent-handler/requirements.txt +5 -0
- package/lib/framework/agents/resources/interactive-agent-handler/index.py +16 -93
- package/lib/framework/agents/resources/interactive-agent-handler/test_handler.py +214 -413
- package/lib/framework/bedrock/bedrock.js +1 -1
- package/lib/framework/custom-resource/default-runtimes.js +1 -1
- package/lib/framework/foundation/access-log.js +1 -1
- package/lib/framework/foundation/eventbridge-broker.js +1 -1
- package/lib/framework/foundation/network.js +1 -1
- package/lib/framework/tests/interactive-agent-nag.test.js +56 -1
- package/lib/framework/tests/interactive-agent.test.js +257 -11
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utilities/data-loader.js +1 -1
- package/lib/utilities/lambda-iam-utils.js +1 -1
- package/lib/utilities/observability/cloudfront-distribution-observability-property-injector.js +1 -1
- package/lib/utilities/observability/cloudwatch-transaction-search.js +1 -1
- package/lib/utilities/observability/default-observability-config.js +1 -1
- package/lib/utilities/observability/lambda-observability-property-injector.js +1 -1
- package/lib/utilities/observability/log-group-data-protection-utils.js +1 -1
- package/lib/utilities/observability/powertools-config.js +1 -1
- package/lib/utilities/observability/state-machine-observability-property-injector.js +1 -1
- package/lib/webapp/frontend-construct.js +1 -1
- package/package.json +2 -2
|
@@ -2,22 +2,12 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
Unit tests for Interactive Agent
|
|
5
|
+
Unit tests for Interactive Agent FastAPI Handler with Strands-native session/context.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
import json
|
|
10
10
|
from unittest.mock import Mock, patch, AsyncMock, MagicMock
|
|
11
|
-
from index import (
|
|
12
|
-
handler,
|
|
13
|
-
handle_connect,
|
|
14
|
-
handle_disconnect,
|
|
15
|
-
handle_message,
|
|
16
|
-
SessionManager,
|
|
17
|
-
ContextManager,
|
|
18
|
-
send_to_connection,
|
|
19
|
-
load_system_prompt
|
|
20
|
-
)
|
|
21
11
|
|
|
22
12
|
|
|
23
13
|
# Fixtures
|
|
@@ -29,71 +19,6 @@ def mock_s3_client():
|
|
|
29
19
|
yield mock
|
|
30
20
|
|
|
31
21
|
|
|
32
|
-
@pytest.fixture
|
|
33
|
-
def mock_apigateway_client():
|
|
34
|
-
"""Mock API Gateway Management API client"""
|
|
35
|
-
with patch('index.apigateway_client') as mock:
|
|
36
|
-
yield mock
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@pytest.fixture
|
|
40
|
-
def connect_event():
|
|
41
|
-
"""Sample $connect event"""
|
|
42
|
-
return {
|
|
43
|
-
'requestContext': {
|
|
44
|
-
'routeKey': '$connect',
|
|
45
|
-
'connectionId': 'test-connection-123',
|
|
46
|
-
'requestTimeEpoch': 1234567890,
|
|
47
|
-
'authorizer': {
|
|
48
|
-
'principalId': 'user-123'
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@pytest.fixture
|
|
55
|
-
def disconnect_event():
|
|
56
|
-
"""Sample $disconnect event"""
|
|
57
|
-
return {
|
|
58
|
-
'requestContext': {
|
|
59
|
-
'routeKey': '$disconnect',
|
|
60
|
-
'connectionId': 'test-connection-123',
|
|
61
|
-
'authorizer': {
|
|
62
|
-
'principalId': 'user-123'
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@pytest.fixture
|
|
69
|
-
def message_event():
|
|
70
|
-
"""Sample message event"""
|
|
71
|
-
return {
|
|
72
|
-
'requestContext': {
|
|
73
|
-
'routeKey': '$default',
|
|
74
|
-
'connectionId': 'test-connection-123',
|
|
75
|
-
'domainName': 'test.execute-api.us-east-1.amazonaws.com',
|
|
76
|
-
'stage': 'prod',
|
|
77
|
-
'authorizer': {
|
|
78
|
-
'principalId': 'user-123'
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
'body': json.dumps({
|
|
82
|
-
'message': 'Hello, agent!'
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@pytest.fixture
|
|
88
|
-
def lambda_context():
|
|
89
|
-
"""Mock Lambda context"""
|
|
90
|
-
context = Mock()
|
|
91
|
-
context.function_name = 'test-function'
|
|
92
|
-
context.memory_limit_in_mb = 1024
|
|
93
|
-
context.invoked_function_arn = 'arn:aws:lambda:us-east-1:123456789012:function:test-function'
|
|
94
|
-
return context
|
|
95
|
-
|
|
96
|
-
|
|
97
22
|
# Tests for load_system_prompt
|
|
98
23
|
|
|
99
24
|
def test_load_system_prompt_success(mock_s3_client):
|
|
@@ -101,13 +26,14 @@ def test_load_system_prompt_success(mock_s3_client):
|
|
|
101
26
|
mock_s3_client.get_object.return_value = {
|
|
102
27
|
'Body': Mock(read=Mock(return_value=b'You are a helpful assistant.'))
|
|
103
28
|
}
|
|
104
|
-
|
|
29
|
+
|
|
105
30
|
with patch.dict('os.environ', {
|
|
106
31
|
'SYSTEM_PROMPT_S3_BUCKET_NAME': 'test-bucket',
|
|
107
32
|
'SYSTEM_PROMPT_S3_KEY': 'prompts/system.txt'
|
|
108
33
|
}):
|
|
34
|
+
from index import load_system_prompt
|
|
109
35
|
prompt = load_system_prompt()
|
|
110
|
-
|
|
36
|
+
|
|
111
37
|
assert prompt == 'You are a helpful assistant.'
|
|
112
38
|
mock_s3_client.get_object.assert_called_once_with(
|
|
113
39
|
Bucket='test-bucket',
|
|
@@ -118,384 +44,259 @@ def test_load_system_prompt_success(mock_s3_client):
|
|
|
118
44
|
def test_load_system_prompt_missing_config():
|
|
119
45
|
"""Test system prompt loading with missing configuration"""
|
|
120
46
|
with patch.dict('os.environ', {}, clear=True):
|
|
47
|
+
from index import load_system_prompt
|
|
121
48
|
prompt = load_system_prompt()
|
|
122
|
-
|
|
49
|
+
|
|
123
50
|
assert prompt == "You are a helpful AI assistant."
|
|
124
51
|
|
|
125
52
|
|
|
126
53
|
def test_load_system_prompt_s3_error(mock_s3_client):
|
|
127
54
|
"""Test system prompt loading with S3 error"""
|
|
128
55
|
mock_s3_client.get_object.side_effect = Exception('S3 error')
|
|
129
|
-
|
|
56
|
+
|
|
130
57
|
with patch.dict('os.environ', {
|
|
131
58
|
'SYSTEM_PROMPT_S3_BUCKET_NAME': 'test-bucket',
|
|
132
59
|
'SYSTEM_PROMPT_S3_KEY': 'prompts/system.txt'
|
|
133
60
|
}):
|
|
61
|
+
from index import load_system_prompt
|
|
134
62
|
prompt = load_system_prompt()
|
|
135
|
-
|
|
63
|
+
|
|
136
64
|
assert prompt == "You are a helpful AI assistant."
|
|
137
65
|
|
|
138
66
|
|
|
139
|
-
# Tests for
|
|
67
|
+
# Tests for load_tools_from_s3
|
|
140
68
|
|
|
141
|
-
def
|
|
142
|
-
"""Test
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
assert
|
|
69
|
+
def test_load_tools_from_s3_empty_config():
|
|
70
|
+
"""Test tool loading with empty config"""
|
|
71
|
+
from index import load_tools_from_s3
|
|
72
|
+
with patch.dict('os.environ', {'TOOLS_CONFIG': '[]'}):
|
|
73
|
+
tools = load_tools_from_s3()
|
|
74
|
+
assert tools == []
|
|
147
75
|
|
|
148
76
|
|
|
149
|
-
def
|
|
150
|
-
"""Test
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
mock_s3_client.get_object.return_value = {
|
|
156
|
-
'Body': Mock(read=Mock(return_value=json.dumps(session_data).encode('utf-8')))
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
manager = SessionManager(bucket='test-bucket')
|
|
160
|
-
result = manager.get_session('conn-123', 'user-123')
|
|
161
|
-
|
|
162
|
-
assert result == session_data
|
|
163
|
-
mock_s3_client.get_object.assert_called_once_with(
|
|
164
|
-
Bucket='test-bucket',
|
|
165
|
-
Key='sessions/user-123/conn-123.json'
|
|
166
|
-
)
|
|
77
|
+
def test_load_tools_from_s3_invalid_json():
|
|
78
|
+
"""Test tool loading with invalid JSON config"""
|
|
79
|
+
from index import load_tools_from_s3
|
|
80
|
+
with patch.dict('os.environ', {'TOOLS_CONFIG': 'invalid'}):
|
|
81
|
+
tools = load_tools_from_s3()
|
|
82
|
+
assert tools == []
|
|
167
83
|
|
|
168
84
|
|
|
169
|
-
|
|
170
|
-
"""Test session retrieval when session doesn't exist"""
|
|
171
|
-
mock_s3_client.get_object.side_effect = mock_s3_client.exceptions.NoSuchKey('Not found')
|
|
172
|
-
mock_s3_client.exceptions = type('Exceptions', (), {'NoSuchKey': Exception})()
|
|
173
|
-
|
|
174
|
-
manager = SessionManager(bucket='test-bucket')
|
|
175
|
-
result = manager.get_session('conn-123', 'user-123')
|
|
176
|
-
|
|
177
|
-
assert result == {'messages': [], 'metadata': {}}
|
|
85
|
+
# Tests for SSE formatting
|
|
178
86
|
|
|
87
|
+
def test_format_sse_data_only():
|
|
88
|
+
"""Test SSE formatting with data only"""
|
|
89
|
+
from index import format_sse
|
|
90
|
+
result = format_sse('{"text": "hello"}')
|
|
91
|
+
assert result == 'data: {"text": "hello"}\n\n'
|
|
179
92
|
|
|
180
|
-
def test_session_manager_save_session(mock_s3_client):
|
|
181
|
-
"""Test session saving"""
|
|
182
|
-
session_data = {
|
|
183
|
-
'messages': [{'role': 'user', 'content': 'Hello'}],
|
|
184
|
-
'metadata': {'user_id': 'user-123'}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
manager = SessionManager(bucket='test-bucket')
|
|
188
|
-
manager.save_session('conn-123', 'user-123', session_data)
|
|
189
|
-
|
|
190
|
-
mock_s3_client.put_object.assert_called_once()
|
|
191
|
-
call_args = mock_s3_client.put_object.call_args
|
|
192
|
-
assert call_args[1]['Bucket'] == 'test-bucket'
|
|
193
|
-
assert call_args[1]['Key'] == 'sessions/user-123/conn-123.json'
|
|
194
|
-
assert json.loads(call_args[1]['Body']) == session_data
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def test_session_manager_delete_session(mock_s3_client):
|
|
198
|
-
"""Test session deletion"""
|
|
199
|
-
manager = SessionManager(bucket='test-bucket')
|
|
200
|
-
manager.delete_session('conn-123', 'user-123')
|
|
201
|
-
|
|
202
|
-
mock_s3_client.delete_object.assert_called_once_with(
|
|
203
|
-
Bucket='test-bucket',
|
|
204
|
-
Key='sessions/user-123/conn-123.json'
|
|
205
|
-
)
|
|
206
93
|
|
|
94
|
+
def test_format_sse_with_event():
|
|
95
|
+
"""Test SSE formatting with event type"""
|
|
96
|
+
from index import format_sse
|
|
97
|
+
result = format_sse('{}', event='done')
|
|
98
|
+
assert result == 'event: done\ndata: {}\n\n'
|
|
207
99
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def test_context_manager_add_message():
|
|
246
|
-
"""Test adding message to context"""
|
|
247
|
-
manager = ContextManager()
|
|
248
|
-
|
|
249
|
-
messages = []
|
|
250
|
-
messages = manager.add_message(messages, 'user', 'Hello')
|
|
251
|
-
messages = manager.add_message(messages, 'assistant', 'Hi there!')
|
|
252
|
-
|
|
253
|
-
assert len(messages) == 2
|
|
254
|
-
assert messages[0] == {'role': 'user', 'content': 'Hello'}
|
|
255
|
-
assert messages[1] == {'role': 'assistant', 'content': 'Hi there!'}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
# Tests for send_to_connection
|
|
259
|
-
|
|
260
|
-
def test_send_to_connection_success():
|
|
261
|
-
"""Test successful message sending"""
|
|
262
|
-
mock_client = Mock()
|
|
263
|
-
|
|
264
|
-
with patch('boto3.client', return_value=mock_client):
|
|
265
|
-
send_to_connection(
|
|
266
|
-
'https://test.execute-api.us-east-1.amazonaws.com/prod',
|
|
267
|
-
'conn-123',
|
|
268
|
-
{'type': 'chunk', 'content': 'Hello'}
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
mock_client.post_to_connection.assert_called_once()
|
|
272
|
-
call_args = mock_client.post_to_connection.call_args
|
|
273
|
-
assert call_args[1]['ConnectionId'] == 'conn-123'
|
|
274
|
-
assert json.loads(call_args[1]['Data']) == {'type': 'chunk', 'content': 'Hello'}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def test_send_to_connection_gone():
|
|
278
|
-
"""Test sending to gone connection"""
|
|
279
|
-
mock_client = Mock()
|
|
280
|
-
mock_client.post_to_connection.side_effect = mock_client.exceptions.GoneException('Gone')
|
|
281
|
-
mock_client.exceptions = type('Exceptions', (), {'GoneException': Exception})()
|
|
282
|
-
|
|
283
|
-
with patch('boto3.client', return_value=mock_client):
|
|
284
|
-
# Should not raise exception
|
|
285
|
-
send_to_connection(
|
|
286
|
-
'https://test.execute-api.us-east-1.amazonaws.com/prod',
|
|
287
|
-
'conn-123',
|
|
288
|
-
{'type': 'chunk', 'content': 'Hello'}
|
|
100
|
+
|
|
101
|
+
# Tests for chat endpoint
|
|
102
|
+
|
|
103
|
+
@pytest.mark.asyncio
|
|
104
|
+
async def test_chat_creates_strands_session_manager():
|
|
105
|
+
"""Test that chat creates Strands S3SessionManager when SESSION_BUCKET is set"""
|
|
106
|
+
from index import app
|
|
107
|
+
from httpx import AsyncClient, ASGITransport
|
|
108
|
+
|
|
109
|
+
mock_agent_instance = MagicMock()
|
|
110
|
+
|
|
111
|
+
# Mock stream_async to yield chunks and then return
|
|
112
|
+
async def mock_stream_async(msg):
|
|
113
|
+
yield {'data': 'Hello!'}
|
|
114
|
+
|
|
115
|
+
mock_agent_instance.stream_async = mock_stream_async
|
|
116
|
+
|
|
117
|
+
with patch('index.SESSION_BUCKET', 'test-bucket'), \
|
|
118
|
+
patch('index.StrandsS3SessionManager') as mock_session_cls, \
|
|
119
|
+
patch('index.SlidingWindowConversationManager') as mock_conv_cls, \
|
|
120
|
+
patch('index.Agent', return_value=mock_agent_instance) as mock_agent_cls:
|
|
121
|
+
|
|
122
|
+
transport = ASGITransport(app=app)
|
|
123
|
+
async with AsyncClient(transport=transport, base_url='http://test') as client:
|
|
124
|
+
response = await client.post('/chat', json={
|
|
125
|
+
'message': 'Hello',
|
|
126
|
+
'session_id': 'test-session-123',
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
assert response.status_code == 200
|
|
130
|
+
|
|
131
|
+
# Verify Strands S3SessionManager was created with correct params
|
|
132
|
+
mock_session_cls.assert_called_once_with(
|
|
133
|
+
session_id='test-session-123',
|
|
134
|
+
bucket_name='test-bucket',
|
|
289
135
|
)
|
|
290
136
|
|
|
137
|
+
# Verify SlidingWindowConversationManager was created
|
|
138
|
+
mock_conv_cls.assert_called_once_with(window_size=20)
|
|
139
|
+
|
|
140
|
+
# Verify Agent was created with session_manager and conversation_manager
|
|
141
|
+
mock_agent_cls.assert_called_once()
|
|
142
|
+
call_kwargs = mock_agent_cls.call_args[1]
|
|
143
|
+
assert 'session_manager' in call_kwargs
|
|
144
|
+
assert 'conversation_manager' in call_kwargs
|
|
145
|
+
assert call_kwargs['callback_handler'] is None
|
|
146
|
+
|
|
291
147
|
|
|
292
|
-
|
|
148
|
+
@pytest.mark.asyncio
|
|
149
|
+
async def test_chat_no_session_bucket():
|
|
150
|
+
"""Test that chat works without SESSION_BUCKET (no session persistence)"""
|
|
151
|
+
from index import app
|
|
152
|
+
from httpx import AsyncClient, ASGITransport
|
|
293
153
|
|
|
294
|
-
|
|
295
|
-
"""Test successful connection handling"""
|
|
296
|
-
with patch.dict('os.environ', {'AUTH_TYPE': 'Cognito', 'SESSION_BUCKET': 'test-bucket'}):
|
|
297
|
-
response = handle_connect(connect_event)
|
|
298
|
-
|
|
299
|
-
assert response['statusCode'] == 200
|
|
300
|
-
assert 'Connected' in response['body']
|
|
301
|
-
mock_s3_client.put_object.assert_called_once()
|
|
154
|
+
mock_agent_instance = MagicMock()
|
|
302
155
|
|
|
156
|
+
async def mock_stream_async(msg):
|
|
157
|
+
yield {'data': 'Hello!'}
|
|
303
158
|
|
|
304
|
-
|
|
305
|
-
"""Test connection handling without authentication"""
|
|
306
|
-
event = connect_event.copy()
|
|
307
|
-
del event['requestContext']['authorizer']
|
|
308
|
-
|
|
309
|
-
with patch.dict('os.environ', {'AUTH_TYPE': 'None', 'SESSION_BUCKET': 'test-bucket'}):
|
|
310
|
-
response = handle_connect(event)
|
|
311
|
-
|
|
312
|
-
assert response['statusCode'] == 200
|
|
159
|
+
mock_agent_instance.stream_async = mock_stream_async
|
|
313
160
|
|
|
161
|
+
with patch('index.SESSION_BUCKET', None), \
|
|
162
|
+
patch('index.Agent', return_value=mock_agent_instance) as mock_agent_cls:
|
|
314
163
|
|
|
315
|
-
|
|
164
|
+
transport = ASGITransport(app=app)
|
|
165
|
+
async with AsyncClient(transport=transport, base_url='http://test') as client:
|
|
166
|
+
response = await client.post('/chat', json={
|
|
167
|
+
'message': 'Hello',
|
|
168
|
+
})
|
|
316
169
|
|
|
317
|
-
|
|
318
|
-
"""Test successful disconnection handling"""
|
|
319
|
-
with patch.dict('os.environ', {'AUTH_TYPE': 'Cognito', 'SESSION_BUCKET': 'test-bucket'}):
|
|
320
|
-
response = handle_disconnect(disconnect_event)
|
|
321
|
-
|
|
322
|
-
assert response['statusCode'] == 200
|
|
323
|
-
assert 'Disconnected' in response['body']
|
|
324
|
-
mock_s3_client.delete_object.assert_called_once()
|
|
170
|
+
assert response.status_code == 200
|
|
325
171
|
|
|
172
|
+
# Agent should be called with session_manager=None
|
|
173
|
+
call_kwargs = mock_agent_cls.call_args[1]
|
|
174
|
+
assert call_kwargs['session_manager'] is None
|
|
326
175
|
|
|
327
|
-
# Tests for handle_message
|
|
328
176
|
|
|
329
177
|
@pytest.mark.asyncio
|
|
330
|
-
async def
|
|
331
|
-
"""Test
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
assert 'Message processed' in response['body']
|
|
367
|
-
|
|
368
|
-
# Verify chunks were sent
|
|
369
|
-
assert mock_send.call_count >= 3 # At least 3 chunks + complete message
|
|
370
|
-
|
|
371
|
-
# Verify session was saved
|
|
372
|
-
mock_s3_client.put_object.assert_called_once()
|
|
178
|
+
async def test_chat_generates_session_id():
|
|
179
|
+
"""Test that chat generates a session_id when none provided"""
|
|
180
|
+
from index import app
|
|
181
|
+
from httpx import AsyncClient, ASGITransport
|
|
182
|
+
|
|
183
|
+
mock_agent_instance = MagicMock()
|
|
184
|
+
|
|
185
|
+
async def mock_stream_async(msg):
|
|
186
|
+
yield {'data': 'Hi'}
|
|
187
|
+
|
|
188
|
+
mock_agent_instance.stream_async = mock_stream_async
|
|
189
|
+
|
|
190
|
+
with patch('index.SESSION_BUCKET', None), \
|
|
191
|
+
patch('index.Agent', return_value=mock_agent_instance):
|
|
192
|
+
|
|
193
|
+
transport = ASGITransport(app=app)
|
|
194
|
+
async with AsyncClient(transport=transport, base_url='http://test') as client:
|
|
195
|
+
response = await client.post('/chat', json={
|
|
196
|
+
'message': 'Hello',
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
assert response.status_code == 200
|
|
200
|
+
# Parse SSE events to find metadata with session_id
|
|
201
|
+
lines = response.text.strip().split('\n')
|
|
202
|
+
metadata_data = None
|
|
203
|
+
for i, line in enumerate(lines):
|
|
204
|
+
if line.startswith('event: metadata'):
|
|
205
|
+
# Next non-empty line should be data:
|
|
206
|
+
for j in range(i + 1, len(lines)):
|
|
207
|
+
if lines[j].startswith('data: '):
|
|
208
|
+
metadata_data = json.loads(lines[j][6:])
|
|
209
|
+
break
|
|
210
|
+
break
|
|
211
|
+
assert metadata_data is not None
|
|
212
|
+
assert 'session_id' in metadata_data
|
|
213
|
+
assert len(metadata_data['session_id']) > 0
|
|
373
214
|
|
|
374
215
|
|
|
375
216
|
@pytest.mark.asyncio
|
|
376
|
-
async def
|
|
377
|
-
"""Test
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
217
|
+
async def test_chat_streams_sse_events():
|
|
218
|
+
"""Test that chat streams SSE events correctly"""
|
|
219
|
+
from index import app
|
|
220
|
+
from httpx import AsyncClient, ASGITransport
|
|
221
|
+
|
|
222
|
+
mock_agent_instance = MagicMock()
|
|
223
|
+
|
|
224
|
+
async def mock_stream_async(msg):
|
|
225
|
+
yield {'data': 'Hello '}
|
|
226
|
+
yield {'data': 'world!'}
|
|
227
|
+
|
|
228
|
+
mock_agent_instance.stream_async = mock_stream_async
|
|
229
|
+
|
|
230
|
+
with patch('index.SESSION_BUCKET', None), \
|
|
231
|
+
patch('index.Agent', return_value=mock_agent_instance):
|
|
232
|
+
|
|
233
|
+
transport = ASGITransport(app=app)
|
|
234
|
+
async with AsyncClient(transport=transport, base_url='http://test') as client:
|
|
235
|
+
response = await client.post('/chat', json={
|
|
236
|
+
'message': 'Hello',
|
|
237
|
+
'session_id': 'test-session',
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
assert response.status_code == 200
|
|
241
|
+
body = response.text
|
|
242
|
+
|
|
243
|
+
# Should contain metadata, text chunks, and done events
|
|
244
|
+
assert 'event: metadata' in body
|
|
245
|
+
assert '"text": "Hello "' in body
|
|
246
|
+
assert '"text": "world!"' in body
|
|
247
|
+
assert 'event: done' in body
|
|
391
248
|
|
|
392
249
|
|
|
393
250
|
@pytest.mark.asyncio
|
|
394
|
-
async def
|
|
395
|
-
"""Test
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
251
|
+
async def test_chat_handles_agent_error():
|
|
252
|
+
"""Test that chat handles agent errors gracefully"""
|
|
253
|
+
from index import app
|
|
254
|
+
from httpx import AsyncClient, ASGITransport
|
|
255
|
+
|
|
256
|
+
mock_agent_instance = MagicMock()
|
|
257
|
+
|
|
258
|
+
async def mock_stream_async(msg):
|
|
259
|
+
raise Exception('Agent error')
|
|
260
|
+
yield # Make it an async generator
|
|
261
|
+
|
|
262
|
+
mock_agent_instance.stream_async = mock_stream_async
|
|
263
|
+
|
|
264
|
+
with patch('index.SESSION_BUCKET', None), \
|
|
265
|
+
patch('index.Agent', return_value=mock_agent_instance):
|
|
404
266
|
|
|
267
|
+
transport = ASGITransport(app=app)
|
|
268
|
+
async with AsyncClient(transport=transport, base_url='http://test') as client:
|
|
269
|
+
response = await client.post('/chat', json={
|
|
270
|
+
'message': 'Hello',
|
|
271
|
+
'session_id': 'test-session',
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
assert response.status_code == 200
|
|
275
|
+
body = response.text
|
|
276
|
+
assert 'event: error' in body
|
|
277
|
+
assert 'internal error' in body.lower() or 'error' in body.lower()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# Tests for health endpoint
|
|
405
281
|
|
|
406
282
|
@pytest.mark.asyncio
|
|
407
|
-
async def
|
|
408
|
-
"""Test
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}):
|
|
428
|
-
|
|
429
|
-
response = await handle_message(message_event)
|
|
430
|
-
|
|
431
|
-
assert response['statusCode'] == 500
|
|
432
|
-
assert 'Failed to process message' in response['body']
|
|
433
|
-
|
|
434
|
-
# Verify error was sent to client
|
|
435
|
-
error_calls = [call for call in mock_send.call_args_list
|
|
436
|
-
if call[0][2].get('type') == 'error']
|
|
437
|
-
assert len(error_calls) > 0
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
# Tests for main handler
|
|
441
|
-
|
|
442
|
-
def test_handler_connect_route(connect_event, lambda_context, mock_s3_client):
|
|
443
|
-
"""Test handler routing for $connect"""
|
|
444
|
-
with patch.dict('os.environ', {'SESSION_BUCKET': 'test-bucket'}):
|
|
445
|
-
response = handler(connect_event, lambda_context)
|
|
446
|
-
|
|
447
|
-
assert response['statusCode'] == 200
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
def test_handler_disconnect_route(disconnect_event, lambda_context, mock_s3_client):
|
|
451
|
-
"""Test handler routing for $disconnect"""
|
|
452
|
-
with patch.dict('os.environ', {'SESSION_BUCKET': 'test-bucket'}):
|
|
453
|
-
response = handler(disconnect_event, lambda_context)
|
|
454
|
-
|
|
455
|
-
assert response['statusCode'] == 200
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
def test_handler_message_route(message_event, lambda_context, mock_s3_client):
|
|
459
|
-
"""Test handler routing for message"""
|
|
460
|
-
# Mock session data
|
|
461
|
-
mock_s3_client.get_object.return_value = {
|
|
462
|
-
'Body': Mock(read=Mock(return_value=json.dumps({
|
|
463
|
-
'messages': [],
|
|
464
|
-
'metadata': {'user_id': 'user-123'}
|
|
465
|
-
}).encode('utf-8')))
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
# Mock BidiAgent
|
|
469
|
-
mock_agent = AsyncMock()
|
|
470
|
-
|
|
471
|
-
async def mock_stream(*args, **kwargs):
|
|
472
|
-
yield {'content': 'Hello'}
|
|
473
|
-
|
|
474
|
-
mock_agent.stream = mock_stream
|
|
475
|
-
|
|
476
|
-
with patch('index.BidiAgent', return_value=mock_agent), \
|
|
477
|
-
patch('index.send_to_connection'), \
|
|
478
|
-
patch.dict('os.environ', {
|
|
479
|
-
'SESSION_BUCKET': 'test-bucket',
|
|
480
|
-
'CONTEXT_ENABLED': 'true',
|
|
481
|
-
'AUTH_TYPE': 'Cognito'
|
|
482
|
-
}):
|
|
483
|
-
|
|
484
|
-
response = handler(message_event, lambda_context)
|
|
485
|
-
|
|
486
|
-
assert response['statusCode'] == 200
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
def test_handler_unknown_route(lambda_context):
|
|
490
|
-
"""Test handler with unknown route"""
|
|
491
|
-
event = {
|
|
492
|
-
'requestContext': {
|
|
493
|
-
'routeKey': 'unknown',
|
|
494
|
-
'connectionId': 'test-connection-123'
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
response = handler(event, lambda_context)
|
|
499
|
-
|
|
500
|
-
assert response['statusCode'] == 400
|
|
501
|
-
assert 'Unknown route' in response['body']
|
|
283
|
+
async def test_health_endpoint():
|
|
284
|
+
"""Test health check endpoint"""
|
|
285
|
+
from index import app
|
|
286
|
+
from httpx import AsyncClient, ASGITransport
|
|
287
|
+
|
|
288
|
+
transport = ASGITransport(app=app)
|
|
289
|
+
async with AsyncClient(transport=transport, base_url='http://test') as client:
|
|
290
|
+
response = await client.get('/health')
|
|
291
|
+
|
|
292
|
+
assert response.status_code == 200
|
|
293
|
+
assert response.json() == {'status': 'ok'}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Tests for handler function (Lambda compatibility)
|
|
297
|
+
|
|
298
|
+
def test_handler_returns_200():
|
|
299
|
+
"""Test Lambda handler fallback"""
|
|
300
|
+
from index import handler
|
|
301
|
+
result = handler({}, None)
|
|
302
|
+
assert result['statusCode'] == 200
|