@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.
Files changed (39) hide show
  1. package/.jsii +1276 -211
  2. package/lib/document-processing/adapter/queued-s3-adapter.js +1 -1
  3. package/lib/document-processing/agentic-document-processing.js +2 -2
  4. package/lib/document-processing/base-document-processing.js +1 -1
  5. package/lib/document-processing/bedrock-document-processing.js +1 -1
  6. package/lib/document-processing/default-document-processing-config.js +1 -1
  7. package/lib/framework/agents/base-agent.d.ts +15 -2
  8. package/lib/framework/agents/base-agent.js +3 -3
  9. package/lib/framework/agents/batch-agent.d.ts +1 -1
  10. package/lib/framework/agents/batch-agent.js +2 -2
  11. package/lib/framework/agents/default-agent-config.js +1 -1
  12. package/lib/framework/agents/interactive-agent.d.ts +286 -3
  13. package/lib/framework/agents/interactive-agent.js +412 -123
  14. package/lib/framework/agents/knowledge-base/base-knowledge-base.js +1 -1
  15. package/lib/framework/agents/knowledge-base/bedrock-knowledge-base.js +1 -1
  16. package/lib/framework/agents/resources/agentcore-agent-handler/Dockerfile +20 -0
  17. package/lib/framework/agents/resources/agentcore-agent-handler/main.py +224 -0
  18. package/lib/framework/agents/resources/agentcore-agent-handler/requirements.txt +5 -0
  19. package/lib/framework/agents/resources/interactive-agent-handler/index.py +16 -93
  20. package/lib/framework/agents/resources/interactive-agent-handler/test_handler.py +214 -413
  21. package/lib/framework/bedrock/bedrock.js +1 -1
  22. package/lib/framework/custom-resource/default-runtimes.js +1 -1
  23. package/lib/framework/foundation/access-log.js +1 -1
  24. package/lib/framework/foundation/eventbridge-broker.js +1 -1
  25. package/lib/framework/foundation/network.js +1 -1
  26. package/lib/framework/tests/interactive-agent-nag.test.js +56 -1
  27. package/lib/framework/tests/interactive-agent.test.js +257 -11
  28. package/lib/tsconfig.tsbuildinfo +1 -1
  29. package/lib/utilities/data-loader.js +1 -1
  30. package/lib/utilities/lambda-iam-utils.js +1 -1
  31. package/lib/utilities/observability/cloudfront-distribution-observability-property-injector.js +1 -1
  32. package/lib/utilities/observability/cloudwatch-transaction-search.js +1 -1
  33. package/lib/utilities/observability/default-observability-config.js +1 -1
  34. package/lib/utilities/observability/lambda-observability-property-injector.js +1 -1
  35. package/lib/utilities/observability/log-group-data-protection-utils.js +1 -1
  36. package/lib/utilities/observability/powertools-config.js +1 -1
  37. package/lib/utilities/observability/state-machine-observability-property-injector.js +1 -1
  38. package/lib/webapp/frontend-construct.js +1 -1
  39. 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 Lambda Handler
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 SessionManager
67
+ # Tests for load_tools_from_s3
140
68
 
141
- def test_session_manager_disabled():
142
- """Test SessionManager with no bucket (disabled)"""
143
- manager = SessionManager(bucket=None)
144
-
145
- assert not manager.enabled
146
- assert manager.get_session('conn-123', 'user-123') == {'messages': [], 'metadata': {}}
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 test_session_manager_get_session_success(mock_s3_client):
150
- """Test successful session retrieval"""
151
- session_data = {
152
- 'messages': [{'role': 'user', 'content': 'Hello'}],
153
- 'metadata': {'user_id': 'user-123'}
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
- def test_session_manager_get_session_not_found(mock_s3_client):
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
- # Tests for ContextManager
209
-
210
- def test_context_manager_disabled():
211
- """Test ContextManager with context disabled"""
212
- with patch.dict('os.environ', {'CONTEXT_ENABLED': 'false'}):
213
- manager = ContextManager()
214
-
215
- messages = [{'role': 'user', 'content': 'Hello'}]
216
- assert manager.get_context(messages) == []
217
- assert manager.add_message(messages, 'assistant', 'Hi') == messages
218
-
219
-
220
- def test_context_manager_sliding_window():
221
- """Test SlidingWindow context strategy"""
222
- manager = ContextManager(strategy='SlidingWindow', window_size=3)
223
-
224
- messages = [
225
- {'role': 'user', 'content': 'Message 1'},
226
- {'role': 'assistant', 'content': 'Response 1'},
227
- {'role': 'user', 'content': 'Message 2'},
228
- {'role': 'assistant', 'content': 'Response 2'},
229
- {'role': 'user', 'content': 'Message 3'},
230
- ]
231
-
232
- context = manager.get_context(messages)
233
- assert len(context) == 3
234
- assert context[0]['content'] == 'Message 2'
235
-
236
-
237
- def test_context_manager_null_strategy():
238
- """Test Null context strategy"""
239
- manager = ContextManager(strategy='Null')
240
-
241
- messages = [{'role': 'user', 'content': 'Hello'}]
242
- assert manager.get_context(messages) == []
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
- # Tests for handle_connect
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
- def test_handle_connect_success(connect_event, mock_s3_client):
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
- def test_handle_connect_anonymous(connect_event, mock_s3_client):
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
- # Tests for handle_disconnect
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
- def test_handle_disconnect_success(disconnect_event, mock_s3_client):
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 test_handle_message_success(message_event, mock_s3_client):
331
- """Test successful message handling with BidiAgent"""
332
- # Mock session data
333
- mock_s3_client.get_object.return_value = {
334
- 'Body': Mock(read=Mock(return_value=json.dumps({
335
- 'messages': [],
336
- 'metadata': {'user_id': 'user-123'}
337
- }).encode('utf-8')))
338
- }
339
-
340
- # Mock BidiAgent
341
- mock_agent = AsyncMock()
342
- mock_chunks = [
343
- {'content': 'Hello '},
344
- {'content': 'there! '},
345
- {'content': 'How can I help?'}
346
- ]
347
-
348
- async def mock_stream(*args, **kwargs):
349
- for chunk in mock_chunks:
350
- yield chunk
351
-
352
- mock_agent.stream = mock_stream
353
-
354
- # Mock send_to_connection
355
- with patch('index.BidiAgent', return_value=mock_agent), \
356
- patch('index.send_to_connection') as mock_send, \
357
- patch.dict('os.environ', {
358
- 'SESSION_BUCKET': 'test-bucket',
359
- 'CONTEXT_ENABLED': 'true',
360
- 'AUTH_TYPE': 'Cognito'
361
- }):
362
-
363
- response = await handle_message(message_event)
364
-
365
- assert response['statusCode'] == 200
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 test_handle_message_invalid_json(message_event):
377
- """Test message handling with invalid JSON"""
378
- event = message_event.copy()
379
- event['body'] = 'invalid json'
380
-
381
- with patch('index.send_to_connection') as mock_send:
382
- response = await handle_message(event)
383
-
384
- assert response['statusCode'] == 400
385
- assert 'Invalid JSON' in response['body']
386
-
387
- # Verify error was sent to client
388
- mock_send.assert_called_once()
389
- call_args = mock_send.call_args[0]
390
- assert call_args[2]['type'] == 'error'
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 test_handle_message_empty_message(message_event):
395
- """Test message handling with empty message"""
396
- event = message_event.copy()
397
- event['body'] = json.dumps({'message': ''})
398
-
399
- with patch('index.send_to_connection') as mock_send:
400
- response = await handle_message(event)
401
-
402
- assert response['statusCode'] == 400
403
- assert 'Message is required' in response['body']
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 test_handle_message_agent_error(message_event, mock_s3_client):
408
- """Test message handling with agent error"""
409
- # Mock session data
410
- mock_s3_client.get_object.return_value = {
411
- 'Body': Mock(read=Mock(return_value=json.dumps({
412
- 'messages': [],
413
- 'metadata': {'user_id': 'user-123'}
414
- }).encode('utf-8')))
415
- }
416
-
417
- # Mock BidiAgent to raise error
418
- mock_agent = AsyncMock()
419
- mock_agent.stream.side_effect = Exception('Agent error')
420
-
421
- with patch('index.BidiAgent', return_value=mock_agent), \
422
- patch('index.send_to_connection') as mock_send, \
423
- patch.dict('os.environ', {
424
- 'SESSION_BUCKET': 'test-bucket',
425
- 'CONTEXT_ENABLED': 'true',
426
- 'AUTH_TYPE': 'Cognito'
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