|
18 | 18 | CONVERSATIONAL_MAX_SIZE, |
19 | 19 | AgentCoreMemoryConverter, |
20 | 20 | ) |
21 | | -from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, RetrievalConfig |
| 21 | +from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, PersistenceMode, RetrievalConfig |
22 | 22 | from bedrock_agentcore.memory.integrations.strands.session_manager import ( |
23 | 23 | AgentCoreMemorySessionManager, |
24 | 24 | BufferedMessage, |
@@ -3234,3 +3234,200 @@ def test_default_metadata_plain_strings_normalized(self, mock_memory_client): |
3234 | 3234 |
|
3235 | 3235 | kwargs = mock_memory_client.create_event.call_args[1] |
3236 | 3236 | assert kwargs["metadata"]["project"] == {"stringValue": "atlas"} |
| 3237 | + |
| 3238 | + |
| 3239 | +class TestPersistenceMode: |
| 3240 | + """Test persistence_mode=NONE disables ACM persistence but keeps local state management and LTM retrieval.""" |
| 3241 | + |
| 3242 | + @pytest.fixture |
| 3243 | + def no_persist_config(self): |
| 3244 | + return AgentCoreMemoryConfig( |
| 3245 | + memory_id="test-memory-123", |
| 3246 | + session_id="test-session-456", |
| 3247 | + actor_id="test-actor-789", |
| 3248 | + persistence_mode=PersistenceMode.NONE, |
| 3249 | + ) |
| 3250 | + |
| 3251 | + @pytest.fixture |
| 3252 | + def no_persist_manager(self, no_persist_config, mock_memory_client): |
| 3253 | + return _create_session_manager(no_persist_config, mock_memory_client) |
| 3254 | + |
| 3255 | + # --- Config --- |
| 3256 | + |
| 3257 | + def test_config_defaults_to_full(self): |
| 3258 | + config = AgentCoreMemoryConfig(memory_id="m", session_id="s", actor_id="a") |
| 3259 | + assert config.persistence_mode is PersistenceMode.FULL |
| 3260 | + |
| 3261 | + def test_config_accepts_none_mode(self): |
| 3262 | + config = AgentCoreMemoryConfig(memory_id="m", session_id="s", actor_id="a", persistence_mode=PersistenceMode.NONE) |
| 3263 | + assert config.persistence_mode is PersistenceMode.NONE |
| 3264 | + |
| 3265 | + def test_config_accepts_string_value(self): |
| 3266 | + config = AgentCoreMemoryConfig(memory_id="m", session_id="s", actor_id="a", persistence_mode="NONE") |
| 3267 | + assert config.persistence_mode is PersistenceMode.NONE |
| 3268 | + |
| 3269 | + # --- create_session: local state works, no ACM write --- |
| 3270 | + |
| 3271 | + def test_create_session_returns_session(self, no_persist_manager, mock_memory_client): |
| 3272 | + session = Session(session_id="test-session-456", session_type=SessionType.AGENT) |
| 3273 | + result = no_persist_manager.create_session(session) |
| 3274 | + assert result.session_id == "test-session-456" |
| 3275 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3276 | + |
| 3277 | + def test_create_session_still_validates(self, no_persist_manager): |
| 3278 | + session = Session(session_id="wrong-id", session_type=SessionType.AGENT) |
| 3279 | + with pytest.raises(SessionException, match="Session ID mismatch"): |
| 3280 | + no_persist_manager.create_session(session) |
| 3281 | + |
| 3282 | + # --- create_agent: local cache works, no ACM write --- |
| 3283 | + |
| 3284 | + def test_create_agent_caches_timestamp(self, no_persist_manager, mock_memory_client): |
| 3285 | + agent = SessionAgent(agent_id="a1", state={}, conversation_manager_state={}) |
| 3286 | + no_persist_manager.create_agent("test-session-456", agent) |
| 3287 | + assert "a1" in no_persist_manager._agent_created_at_cache |
| 3288 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3289 | + |
| 3290 | + def test_create_agent_still_validates(self, no_persist_manager): |
| 3291 | + agent = SessionAgent(agent_id="a1", state={}, conversation_manager_state={}) |
| 3292 | + with pytest.raises(SessionException, match="Session ID mismatch"): |
| 3293 | + no_persist_manager.create_agent("wrong-id", agent) |
| 3294 | + |
| 3295 | + # --- update_agent: local cache works, no ACM write --- |
| 3296 | + |
| 3297 | + def test_update_agent_no_acm_write(self, no_persist_manager, mock_memory_client): |
| 3298 | + no_persist_manager._agent_created_at_cache["a1"] = "2024-01-01T00:00:00+00:00" |
| 3299 | + agent = SessionAgent(agent_id="a1", state={"k": "v"}, conversation_manager_state={}) |
| 3300 | + no_persist_manager.update_agent("test-session-456", agent) |
| 3301 | + assert agent.created_at == "2024-01-01T00:00:00+00:00" |
| 3302 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3303 | + |
| 3304 | + # --- create_message: validation works, returns non-None for append_message, no ACM write --- |
| 3305 | + |
| 3306 | + def test_create_message_returns_empty_dict(self, no_persist_manager, mock_memory_client): |
| 3307 | + msg = SessionMessage( |
| 3308 | + message={"role": "user", "content": [{"text": "hi"}]}, |
| 3309 | + message_id=1, |
| 3310 | + created_at="2024-01-01T12:00:00Z", |
| 3311 | + ) |
| 3312 | + result = no_persist_manager.create_message("test-session-456", "a1", msg) |
| 3313 | + assert result == {} |
| 3314 | + mock_memory_client.create_event.assert_not_called() |
| 3315 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3316 | + |
| 3317 | + def test_create_message_still_validates(self, no_persist_manager): |
| 3318 | + msg = SessionMessage( |
| 3319 | + message={"role": "user", "content": [{"text": "hi"}]}, |
| 3320 | + message_id=1, |
| 3321 | + created_at="2024-01-01T12:00:00Z", |
| 3322 | + ) |
| 3323 | + with pytest.raises(SessionException, match="Session ID mismatch"): |
| 3324 | + no_persist_manager.create_message("wrong-id", "a1", msg) |
| 3325 | + |
| 3326 | + def test_create_message_returns_none_for_empty_payload(self, no_persist_manager): |
| 3327 | + msg = SessionMessage( |
| 3328 | + message={"role": "user", "content": []}, |
| 3329 | + message_id=1, |
| 3330 | + created_at="2024-01-01T12:00:00Z", |
| 3331 | + ) |
| 3332 | + result = no_persist_manager.create_message("test-session-456", "a1", msg) |
| 3333 | + assert result is None |
| 3334 | + |
| 3335 | + # --- append_message: local state tracking works --- |
| 3336 | + |
| 3337 | + def test_append_message_tracks_local_state(self, no_persist_manager, mock_memory_client): |
| 3338 | + no_persist_manager._latest_agent_message = {} |
| 3339 | + mock_agent = Mock() |
| 3340 | + mock_agent.agent_id = "a1" |
| 3341 | + message = {"role": "user", "content": [{"text": "hello"}]} |
| 3342 | + no_persist_manager.append_message(message, mock_agent) |
| 3343 | + assert "a1" in no_persist_manager._latest_agent_message |
| 3344 | + mock_memory_client.create_event.assert_not_called() |
| 3345 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3346 | + |
| 3347 | + # --- flush: no-ops --- |
| 3348 | + |
| 3349 | + def test_flush_messages_only_noop(self, no_persist_manager, mock_memory_client): |
| 3350 | + assert no_persist_manager._flush_messages_only() == [] |
| 3351 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3352 | + |
| 3353 | + def test_flush_agent_states_only_noop(self, no_persist_manager, mock_memory_client): |
| 3354 | + assert no_persist_manager._flush_agent_states_only() == [] |
| 3355 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3356 | + |
| 3357 | + def test_close_no_acm_writes(self, no_persist_manager, mock_memory_client): |
| 3358 | + no_persist_manager.close() |
| 3359 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3360 | + |
| 3361 | + # --- reads still work --- |
| 3362 | + |
| 3363 | + def test_read_session_works(self, no_persist_manager, mock_memory_client): |
| 3364 | + mock_memory_client.list_events.return_value = [ |
| 3365 | + {"eventId": "e1", "payload": [{"blob": '{"session_id": "test-session-456", "session_type": "AGENT"}'}]}, |
| 3366 | + ] |
| 3367 | + result = no_persist_manager.read_session("test-session-456") |
| 3368 | + assert result is not None |
| 3369 | + assert result.session_id == "test-session-456" |
| 3370 | + |
| 3371 | + def test_read_agent_works(self, no_persist_manager, mock_memory_client): |
| 3372 | + mock_memory_client.list_events.return_value = [ |
| 3373 | + {"eventId": "e1", "payload": [{"blob": '{"agent_id": "a1", "state": {}, "conversation_manager_state": {}}'}]}, |
| 3374 | + ] |
| 3375 | + result = no_persist_manager.read_agent("test-session-456", "a1") |
| 3376 | + assert result is not None |
| 3377 | + assert result.agent_id == "a1" |
| 3378 | + |
| 3379 | + def test_list_messages_works(self, no_persist_manager, mock_memory_client): |
| 3380 | + mock_memory_client.list_events.return_value = [ |
| 3381 | + { |
| 3382 | + "eventId": "e1", |
| 3383 | + "eventTimestamp": "2024-01-01T12:00:00Z", |
| 3384 | + "payload": [{"conversational": {"content": {"text": '{"message": {"role": "user", "content": [{"text": "Hello"}]}, "message_id": 1}'}, "role": "USER"}}], |
| 3385 | + }, |
| 3386 | + ] |
| 3387 | + messages = no_persist_manager.list_messages("test-session-456", "a1") |
| 3388 | + assert len(messages) == 1 |
| 3389 | + |
| 3390 | + # --- legacy migration skipped --- |
| 3391 | + |
| 3392 | + def test_legacy_session_migration_skipped(self, no_persist_manager, mock_memory_client): |
| 3393 | + mock_memory_client.list_events.side_effect = [ |
| 3394 | + [], |
| 3395 | + [{"eventId": "legacy-1", "payload": [{"blob": '{"session_id": "test-session-456", "session_type": "AGENT"}'}]}], |
| 3396 | + ] |
| 3397 | + result = no_persist_manager.read_session("test-session-456") |
| 3398 | + assert result is not None |
| 3399 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3400 | + mock_memory_client.gmdp_client.delete_event.assert_not_called() |
| 3401 | + |
| 3402 | + def test_legacy_agent_migration_skipped(self, no_persist_manager, mock_memory_client): |
| 3403 | + mock_memory_client.list_events.side_effect = [ |
| 3404 | + [], |
| 3405 | + [{"eventId": "legacy-1", "payload": [{"blob": '{"agent_id": "a1", "state": {}, "conversation_manager_state": {}}'}]}], |
| 3406 | + ] |
| 3407 | + result = no_persist_manager.read_agent("test-session-456", "a1") |
| 3408 | + assert result is not None |
| 3409 | + mock_memory_client.gmdp_client.create_event.assert_not_called() |
| 3410 | + mock_memory_client.gmdp_client.delete_event.assert_not_called() |
| 3411 | + |
| 3412 | + # --- LTM retrieval still works --- |
| 3413 | + |
| 3414 | + def test_retrieve_customer_context_works(self, mock_memory_client): |
| 3415 | + config = AgentCoreMemoryConfig( |
| 3416 | + memory_id="test-memory-123", |
| 3417 | + session_id="test-session-456", |
| 3418 | + actor_id="test-actor-789", |
| 3419 | + persistence_mode=PersistenceMode.NONE, |
| 3420 | + retrieval_config={"ns/": RetrievalConfig(top_k=5, relevance_score=0.3)}, |
| 3421 | + ) |
| 3422 | + manager = _create_session_manager(config, mock_memory_client) |
| 3423 | + mock_memory_client.retrieve_memories.return_value = [ |
| 3424 | + {"content": {"text": "remembered fact"}}, |
| 3425 | + ] |
| 3426 | + |
| 3427 | + mock_agent = Mock() |
| 3428 | + mock_agent.messages = [{"role": "user", "content": [{"text": "query"}]}] |
| 3429 | + event = MessageAddedEvent(agent=mock_agent, message={"role": "user", "content": [{"text": "query"}]}) |
| 3430 | + manager.retrieve_customer_context(event) |
| 3431 | + |
| 3432 | + mock_memory_client.retrieve_memories.assert_called_once() |
| 3433 | + assert "<user_context>" in mock_agent.messages[0]["content"][0]["text"] |
0 commit comments