from unittest.mock import AsyncMock, patch import pytest from messaging.handler import ClaudeMessageHandler from messaging.models import IncomingMessage from messaging.session import SessionStore from messaging.trees.queue_manager import TreeQueueManager @pytest.mark.asyncio async def test_reply_to_old_status_message_after_restore_routes_to_parent( tmp_path, mock_platform, mock_cli_manager ): # Build a persisted tree with a root node A and a bot status message id. store_path = tmp_path / "sessions.json" store = SessionStore(storage_path=str(store_path)) handler1 = ClaudeMessageHandler(mock_platform, mock_cli_manager, store) a_incoming = IncomingMessage( text="A", chat_id="chat_1", user_id="user_1", message_id="A", platform="telegram", ) tree = await handler1.tree_queue.create_tree( "A", a_incoming, status_message_id="status_A" ) handler1.tree_queue.register_node("status_A", tree.root_id) store.register_node("status_A", tree.root_id) store.save_tree(tree.root_id, tree.to_dict()) store.flush_pending_save() # "Restart": new store instance loads from disk, and we restore TreeQueueManager. store2 = SessionStore(storage_path=str(store_path)) handler2 = ClaudeMessageHandler(mock_platform, mock_cli_manager, store2) handler2.replace_tree_queue( TreeQueueManager.from_dict( { "trees": store2.get_all_trees(), "node_to_tree": store2.get_node_mapping(), }, queue_update_callback=handler2.update_queue_positions, node_started_callback=handler2.mark_node_processing, ) ) # Prevent background task scheduling; we only want to validate routing/tree mutation. mock_platform.queue_send_message = AsyncMock(return_value="status_reply") reply = IncomingMessage( text="R1", chat_id="chat_1", user_id="user_1", message_id="R1", platform="telegram", reply_to_message_id="status_A", ) with patch.object(handler2.tree_queue, "enqueue", AsyncMock(return_value=False)): await handler2.handle_message(reply) restored_tree = handler2.tree_queue.get_tree_for_node("A") assert restored_tree is not None node_r1 = restored_tree.get_node("R1") assert node_r1 is not None assert node_r1.parent_id == "A" @pytest.mark.asyncio async def test_reply_to_old_status_message_without_mapping_creates_new_conversation( tmp_path, mock_platform, mock_cli_manager ): store_path = tmp_path / "sessions.json" store = SessionStore(storage_path=str(store_path)) handler1 = ClaudeMessageHandler(mock_platform, mock_cli_manager, store) a_incoming = IncomingMessage( text="A", chat_id="chat_1", user_id="user_1", message_id="A", platform="telegram", ) tree = await handler1.tree_queue.create_tree( "A", a_incoming, status_message_id="status_A" ) # Intentionally do NOT register "status_A" mapping. store.save_tree(tree.root_id, tree.to_dict()) store.flush_pending_save() store2 = SessionStore(storage_path=str(store_path)) handler2 = ClaudeMessageHandler(mock_platform, mock_cli_manager, store2) handler2.replace_tree_queue( TreeQueueManager.from_dict( { "trees": store2.get_all_trees(), "node_to_tree": store2.get_node_mapping(), }, queue_update_callback=handler2.update_queue_positions, node_started_callback=handler2.mark_node_processing, ) ) mock_platform.queue_send_message = AsyncMock(return_value="status_reply") reply = IncomingMessage( text="R1", chat_id="chat_1", user_id="user_1", message_id="R1", platform="telegram", reply_to_message_id="status_A", ) with patch.object(handler2.tree_queue, "enqueue", AsyncMock(return_value=False)): await handler2.handle_message(reply) # Since the mapping is missing, this should be treated as a new conversation. new_tree = handler2.tree_queue.get_tree_for_node("R1") assert new_tree is not None assert new_tree.root_id == "R1"