import os import unittest from types import SimpleNamespace from unittest.mock import AsyncMock, patch from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool os.environ.setdefault("DEBUG", "false") from datetime import datetime, timedelta from app.core.time_utils import utc_now from app.db.mock_database import MockBase from app.db.mock_models import RentalContract, RentalPayment, RentalVehicle from app.services.integrations.events import ORDER_CREATED_EVENT from app.services.orchestration.conversation_policy import ConversationPolicy from app.services.orchestration.entity_normalizer import EntityNormalizer from app.services.orchestration.message_planner import MessagePlanner from app.services.orchestration.orquestrador_service import OrquestradorService from app.services.orchestration.tool_executor import ToolExecutor class FakeLLM: def __init__(self, responses): self.responses = list(responses) self.calls = 0 async def generate_response(self, message: str, tools): self.calls += 1 if self.responses: return self.responses.pop(0) return {"response": "", "tool_call": None} class FakeState: def __init__(self, entries=None, contexts=None): self.entries = entries or {} self.contexts = contexts or {} def get_entry(self, bucket: str, user_id: int | None, *, expire: bool = False): if user_id is None: return None return self.entries.get(bucket, {}).get(user_id) def set_entry(self, bucket: str, user_id: int | None, value: dict): if user_id is None: return self.entries.setdefault(bucket, {})[user_id] = value def pop_entry(self, bucket: str, user_id: int | None): if user_id is None: return None return self.entries.get(bucket, {}).pop(user_id, None) def get_user_context(self, user_id: int | None): if user_id is None: return None return self.contexts.get(user_id) def save_user_context(self, user_id: int | None, context: dict): if user_id is None: return self.contexts[user_id] = context class FakeToolExecutor: def __init__(self, result=None): self.result = result or {"ok": True} self.calls = [] async def execute(self, tool_name: str, arguments: dict, user_id: int | None = None): self.calls.append((tool_name, arguments, user_id)) if tool_name == "consultar_estoque" and arguments.get("preco_max") and float(arguments["preco_max"]) > 50000: return [ {"id": 7, "modelo": "Hyundai HB20 2022", "categoria": "hatch", "preco": 54500.0}, {"id": 8, "modelo": "Chevrolet Onix 2023", "categoria": "hatch", "preco": 58900.0}, ] return self.result def coerce_http_error(self, exc): detail = exc.detail if isinstance(exc.detail, dict) else {} return { "code": detail.get("code", "tool_error"), "message": detail.get("message", str(exc)), "retryable": bool(detail.get("retryable", False)), "field": detail.get("field"), } class StaticToolRegistry: def __init__(self, result=None): self.result = result if result is not None else {"ok": True} self.calls = [] async def execute(self, tool_name: str, arguments: dict, user_id: int | None = None): self.calls.append((tool_name, arguments, user_id)) return self.result class FakePolicyService: def __init__(self, state): self.state = state self.normalizer = EntityNormalizer() def _get_user_context(self, user_id: int | None): if user_id is None: return None return self.state.contexts.get(user_id) def _save_user_context(self, user_id: int | None, context: dict | None) -> None: if user_id is None or not isinstance(context, dict): return self.state.save_user_context(user_id, context) def _new_tab_memory(self, user_id: int | None): return {} def _coerce_extraction_contract(self, payload): return payload if isinstance(payload, dict) else self.normalizer.empty_extraction_payload() def _is_affirmative_message(self, text: str) -> bool: normalized = self.normalizer.normalize_text(text).strip().rstrip(".!?,;:") return normalized in {"sim", "pode", "ok", "confirmo", "aceito", "fechado", "pode sim"} def _is_negative_message(self, text: str) -> bool: normalized = self.normalizer.normalize_text(text).strip().rstrip(".!?,;:") return normalized in {"nao", "nao quero"} or normalized.startswith("nao") def _clear_user_conversation_state(self, user_id: int | None) -> None: context = self._get_user_context(user_id) if context: context["pending_order_selection"] = None async def handle_message(self, message: str, user_id: int | None = None) -> str: return f"handled:{message}" def _render_missing_review_fields_prompt(self, missing_fields: list[str]) -> str: return "missing review" def _render_missing_review_reschedule_fields_prompt(self, missing_fields: list[str]) -> str: return "missing review reschedule" def _render_missing_review_cancel_fields_prompt(self, missing_fields: list[str]) -> str: return "missing review cancel" def _render_review_reuse_question(self) -> str: return "reuse review?" def _render_missing_order_fields_prompt(self, missing_fields: list[str]) -> str: return "missing order" def _render_missing_cancel_order_fields_prompt(self, missing_fields: list[str]) -> str: return "missing cancel order" class TurnDecisionContractTests(unittest.IsolatedAsyncioTestCase): async def test_extract_turn_decision_retries_once_and_returns_structured_payload(self): llm = FakeLLM( [ {"response": "nao eh json", "tool_call": None}, { "response": """ { "intent": "review_schedule", "domain": "review", "action": "ask_missing_fields", "entities": { "generic_memory": {}, "review_fields": {"placa": "abc1234", "data_hora": "10/03/2026 \u00e0s 09:00"}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {} }, "missing_fields": ["modelo", "ano", "km"], "tool_name": null, "tool_arguments": {}, "response_to_user": "Preciso do modelo, ano e quilometragem." } """, "tool_call": None, }, ] ) planner = MessagePlanner(llm=llm, normalizer=EntityNormalizer()) decision = await planner.extract_turn_decision("Quero agendar revis\u00e3o amanh\u00e3 \u00e0s 09:00", user_id=7) self.assertEqual(llm.calls, 2) self.assertEqual(decision["intent"], "review_schedule") self.assertEqual(decision["domain"], "review") self.assertEqual(decision["action"], "ask_missing_fields") self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234") self.assertEqual(decision["entities"]["review_fields"]["data_hora"], "10/03/2026 \u00e0s 09:00") self.assertEqual(decision["missing_fields"], ["modelo", "ano", "km"]) def test_parse_json_object_accepts_python_style_dict_with_trailing_commas(self): normalizer = EntityNormalizer() payload = normalizer.parse_json_object( """ ```json { 'intent': 'review_schedule', 'domain': 'review', 'action': 'answer_user', } ``` """ ) self.assertEqual(payload["intent"], "review_schedule") self.assertEqual(payload["domain"], "review") self.assertEqual(payload["action"], "answer_user") def test_coerce_turn_decision_maps_top_level_aliases_and_embedded_intents(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "domain": "service", "action": "answer", "response": "Certo, vou seguir com a revisao.", "selected_index": "2", "entities": { "generic_memory": {"cpf": "12345678909"}, "review_fields": {"placa": "abc1234"}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {"review_schedule": True}, }, } ) self.assertEqual(decision["intent"], "review_schedule") self.assertEqual(decision["domain"], "review") self.assertEqual(decision["action"], "answer_user") self.assertEqual(decision["response_to_user"], "Certo, vou seguir com a revisao.") self.assertEqual(decision["selection_index"], 2) self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234") self.assertEqual(decision["entities"]["generic_memory"]["cpf"], "12345678909") def test_coerce_turn_decision_rejects_invalid_shape_with_fallback(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "valor_invalido", "domain": "sales", "action": "call_tool", "entities": [], } ) self.assertEqual(decision["intent"], "general") self.assertEqual(decision["domain"], "general") self.assertEqual(decision["action"], "answer_user") self.assertEqual(decision["entities"]["order_fields"], {}) def test_coerce_turn_decision_maps_order_aliases_from_model(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "place_order", "domain": "sales", "action": "answer_user", "entities": { "generic_memory": {"orcamento_max": "70000", "cpf": "12345678909"}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": None, } ) self.assertEqual(decision["intent"], "order_create") self.assertEqual(decision["domain"], "sales") self.assertEqual(decision["action"], "answer_user") self.assertEqual(decision["entities"]["generic_memory"]["orcamento_max"], 70000) self.assertEqual(decision["entities"]["generic_memory"]["cpf"], "12345678909") def test_coerce_turn_decision_converts_vehicle_alias_missing_field_into_order_collection(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "create_order", "domain": "sales", "action": "ask_missing_fields", "entities": { "generic_memory": {"orcamento_max": 70000}, "review_fields": {}, "review_management_fields": {}, "order_fields": {"cpf": "12345678909"}, "cancel_order_fields": {}, }, "missing_fields": ["modelo_carro"], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": "Certo! Para qual modelo de carro voce gostaria de um orcamento de 70 mil?", } ) self.assertEqual(decision["intent"], "order_create") self.assertEqual(decision["action"], "collect_order_create") self.assertEqual(decision["missing_fields"], []) self.assertIsNone(decision["response_to_user"]) def test_coerce_turn_decision_normalizes_cancel_order_tool_argument_aliases(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "order_cancel", "domain": "sales", "action": "call_tool", "tool_name": "cancelar_pedido", "tool_arguments": { "order_id": "PED-20260310113756-DC1540", "reason": "desisti da compra", }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["tool_arguments"]["numero_pedido"], "PED-20260310113756-DC1540") self.assertEqual(decision["tool_arguments"]["motivo"], "desisti da compra") def test_coerce_turn_decision_normalizes_order_tool_name_alias_and_downgrades_incomplete_call(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "order_create", "domain": "sales", "action": "call_tool", "tool_name": "fazer_pedido", "tool_arguments": { "modelo": "Onix 2024", }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["action"], "collect_order_create") self.assertIsNone(decision["tool_name"]) self.assertEqual(decision["tool_arguments"], {}) self.assertEqual(decision["entities"]["order_fields"]["modelo_veiculo"], "Onix 2024") def test_coerce_turn_decision_normalizes_review_tool_name_alias(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "review_schedule", "domain": "review", "action": "call_tool", "tool_name": "marcar_revisao", "tool_arguments": { "placa": "ABC1234", "data_hora": "19/03/2026 09:00", "modelo": "Corolla", "ano": 2020, "km": 30000, "revisao_previa_concessionaria": True, }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["tool_name"], "agendar_revisao") self.assertEqual(decision["tool_arguments"]["placa"], "ABC1234") def test_coerce_turn_decision_normalizes_legacy_review_vehicle_tool_alias(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "review_schedule", "domain": "review", "action": "call_tool", "tool_name": "agendar_revisao_veiculo", "tool_arguments": { "placa_veiculo": "ABC1234", "data_agendamento": "tomorrow", "horario_agendamento": "14:00", "modelo_veiculo": "Onix", "ano_veiculo": 2024, "quilometragem": 12000, "revisao_previa": False, }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["tool_name"], "agendar_revisao") self.assertEqual(decision["tool_arguments"]["placa"], "ABC1234") self.assertEqual(decision["tool_arguments"]["modelo"], "Onix") self.assertEqual(decision["tool_arguments"]["ano"], 2024) self.assertEqual(decision["tool_arguments"]["km"], 12000) self.assertFalse(decision["tool_arguments"]["revisao_previa_concessionaria"]) self.assertTrue(decision["tool_arguments"]["data_hora"].endswith("14:00")) def test_coerce_turn_decision_normalizes_review_schedule_tool_argument_aliases(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "review_schedule", "domain": "review", "action": "call_tool", "tool_name": "agendar_revisao", "tool_arguments": { "placa_veiculo": "ABC1234", "data": "20/03/2026 09:00", "modelo_veiculo": "Corolla", "ano_veiculo": 2020, "quilometragem": 30000, "revisao_previa": True, }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["tool_arguments"]["placa"], "ABC1234") self.assertEqual(decision["tool_arguments"]["data_hora"], "20/03/2026 09:00") self.assertEqual(decision["tool_arguments"]["modelo"], "Corolla") self.assertEqual(decision["tool_arguments"]["ano"], 2020) self.assertEqual(decision["tool_arguments"]["km"], 30000) self.assertTrue(decision["tool_arguments"]["revisao_previa_concessionaria"]) def test_coerce_turn_decision_downgrades_incomplete_review_schedule_tool_call_to_collection(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "review_schedule", "domain": "review", "action": "call_tool", "tool_name": "agendar_revisao", "tool_arguments": { "placa_veiculo": "ABC1234", "modelo_veiculo": "Corolla", "ano_veiculo": 2020, }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["action"], "collect_review_schedule") self.assertIsNone(decision["tool_name"]) self.assertEqual(decision["tool_arguments"], {}) self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234") self.assertEqual(decision["entities"]["review_fields"]["modelo"], "Corolla") self.assertEqual(decision["entities"]["review_fields"]["ano"], 2020) def test_coerce_turn_decision_downgrades_incomplete_review_schedule_tool_call_even_with_general_domain(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "review_schedule", "domain": "general", "action": "call_tool", "tool_name": "agendar_revisao", "tool_arguments": { "placa_veiculo": "ABC1234", "modelo_veiculo": "Corolla", }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["action"], "collect_review_schedule") self.assertIsNone(decision["tool_name"]) self.assertEqual(decision["tool_arguments"], {}) self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234") self.assertEqual(decision["entities"]["review_fields"]["modelo"], "Corolla") def test_coerce_turn_decision_normalizes_review_listing_tool_arguments(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "review_list", "domain": "review", "action": "call_tool", "tool_name": "listar_agendamentos", "tool_arguments": { "vehicle_plate": "abc1234", "schedule_status": "agendado", "limit": 10, "tipo": "revisao", }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["tool_name"], "listar_agendamentos_revisao") self.assertEqual( decision["tool_arguments"], {"placa": "ABC1234", "status": "agendado", "limite": 10}, ) def test_coerce_turn_decision_normalizes_review_management_tool_name_alias(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "review_cancel", "domain": "review", "action": "call_tool", "tool_name": "cancelar_agendamento", "tool_arguments": { "protocolo": "REV-20260313-F754AF27", }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["tool_name"], "cancelar_agendamento_revisao") def test_coerce_turn_decision_downgrades_incomplete_cancel_order_tool_call_to_collection(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "order_cancel", "domain": "sales", "action": "call_tool", "tool_name": "cancelar_pedido", "tool_arguments": { "order_id": "PED-20260310124202-5EF4E9", }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "response_to_user": None, } ) self.assertEqual(decision["action"], "collect_order_cancel") self.assertIsNone(decision["tool_name"]) self.assertEqual(decision["tool_arguments"], {}) self.assertEqual(decision["entities"]["cancel_order_fields"]["numero_pedido"], "PED-20260310124202-5EF4E9") def test_coerce_turn_decision_downgrades_missing_response_ask_missing_fields_to_collection(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "review_schedule", "domain": "review", "action": "ask_missing_fields", "entities": { "generic_memory": {}, "review_fields": {"placa": "ABC1234"}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": ["data e hora", "modelo"], "tool_name": None, "tool_arguments": {}, "response_to_user": "", } ) self.assertEqual(decision["intent"], "review_schedule") self.assertEqual(decision["action"], "collect_review_schedule") self.assertEqual(decision["missing_fields"], ["data_hora", "modelo"]) self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234") def test_coerce_turn_decision_downgrades_call_tool_without_tool_name_to_cancel_order_collection(self): normalizer = EntityNormalizer() decision = normalizer.coerce_turn_decision( { "intent": "order_cancel", "domain": "sales", "action": "call_tool", "arguments": { "order_id": "PED-20260310124202-5EF4E9", "reason": "desisti da compra", }, "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, } ) self.assertEqual(decision["intent"], "order_cancel") self.assertEqual(decision["action"], "collect_order_cancel") self.assertIsNone(decision["tool_name"]) self.assertEqual(decision["tool_arguments"], {}) self.assertEqual(decision["entities"]["cancel_order_fields"]["numero_pedido"], "PED-20260310124202-5EF4E9") self.assertEqual(decision["entities"]["cancel_order_fields"]["motivo"], "desisti da compra") def test_turn_decision_entities_do_not_rebuild_legacy_intents(self): service = OrquestradorService.__new__(OrquestradorService) service.normalizer = EntityNormalizer() extracted = service._extracted_entities_from_turn_decision( { "intent": "order_create", "domain": "sales", "action": "collect_order_create", "entities": { "generic_memory": {"cpf": "12345678909"}, "review_fields": {}, "review_management_fields": {}, "order_fields": {"vehicle_id": 1}, "cancel_order_fields": {}, }, } ) self.assertEqual(extracted["intents"], {}) self.assertEqual(extracted["order_fields"]["vehicle_id"], 1) def test_turn_decision_entity_merge_preserves_generic_memory_from_previous_extraction(self): service = OrquestradorService.__new__(OrquestradorService) service.normalizer = EntityNormalizer() service._empty_extraction_payload = service.normalizer.empty_extraction_payload merged = service._merge_extracted_entities( { "generic_memory": {"orcamento_max": 70000}, "review_fields": {}, "review_management_fields": {}, "order_fields": {"cpf": "12345678909"}, "cancel_order_fields": {}, "intents": {}, }, { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, }, ) self.assertEqual(merged["generic_memory"]["orcamento_max"], 70000) self.assertEqual(merged["order_fields"]["cpf"], "12345678909") def test_capture_successful_review_tool_side_effects_store_last_review_package_for_direct_tool_call(self): state = FakeState( contexts={ 7: { "active_domain": "review", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) service._capture_successful_tool_side_effects( tool_name="agendar_revisao", arguments={ "placa": "ABC1463", "data_hora": "28/04/2026 15:00", "modelo": "Civic", "ano": 2024, "km": 30000, "revisao_previa_concessionaria": True, }, tool_result={ "protocolo": "REV-TESTE-123", "placa": "ABC1463", "data_hora": "28/04/2026 15:00", }, user_id=7, ) cached = state.get_entry("last_review_packages", 7) self.assertIsNotNone(cached) self.assertEqual(cached["payload"]["placa"], "ABC1463") self.assertEqual(cached["payload"]["modelo"], "Civic") self.assertTrue(cached["payload"]["revisao_previa_concessionaria"]) async def test_execute_tool_with_trace_normalizes_direct_review_tool_alias_and_merges_open_draft(self): state = FakeState( entries={ "pending_review_drafts": { 7: { "payload": { "placa": "ABC1463", "modelo": "Civic", "ano": 2024, "km": 30000, "revisao_previa_concessionaria": False, }, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 7: { "active_domain": "review", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.tool_executor = FakeToolExecutor(result={"protocolo": "REV-TESTE-123"}) service._log_turn_event = lambda *args, **kwargs: None result = await service._execute_tool_with_trace( tool_name="agendar_revisao_veiculo", arguments={ "data_agendamento": "tomorrow", "horario_agendamento": "14:00", }, user_id=7, ) self.assertEqual(result["protocolo"], "REV-TESTE-123") self.assertEqual(service.tool_executor.calls[0][0], "agendar_revisao") self.assertEqual(service.tool_executor.calls[0][2], 7) self.assertEqual(service.tool_executor.calls[0][1]["placa"], "ABC1463") self.assertEqual(service.tool_executor.calls[0][1]["modelo"], "Civic") self.assertEqual(service.tool_executor.calls[0][1]["ano"], 2024) self.assertEqual(service.tool_executor.calls[0][1]["km"], 30000) self.assertFalse(service.tool_executor.calls[0][1]["revisao_previa_concessionaria"]) self.assertTrue(service.tool_executor.calls[0][1]["data_hora"].endswith("14:00")) def test_capture_tool_result_context_stores_pending_stock_selection_entry(self): state = FakeState( contexts={ 5: { "active_domain": "sales", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) service._capture_tool_result_context( tool_name="consultar_estoque", tool_result=[ {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True}, {"id": 11, "modelo": "Toyota Corolla 2024", "categoria": "suv", "preco": 76087.0}, ], user_id=5, ) cached = state.get_entry("pending_stock_selections", 5) self.assertIsNotNone(cached) self.assertEqual(cached["payload"][0]["id"], 15) self.assertEqual(cached["payload"][1]["modelo"], "Toyota Corolla 2024") self.assertTrue(cached["payload"][0]["budget_relaxed"]) self.assertFalse(cached["payload"][1]["budget_relaxed"]) def test_entity_merge_can_enrich_message_plan_with_full_extraction(self): service = OrquestradorService.__new__(OrquestradorService) service.normalizer = EntityNormalizer() service._empty_extraction_payload = service.normalizer.empty_extraction_payload merged = service._merge_extracted_entities( { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {"cpf": "12345678909"}, "cancel_order_fields": {}, "intents": {}, }, { "generic_memory": {"orcamento_max": 70000}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, }, ) self.assertEqual(merged["generic_memory"]["orcamento_max"], 70000) self.assertEqual(merged["order_fields"]["cpf"], "12345678909") async def test_missing_sales_search_context_triggers_focused_llm_enrichment(self): service = OrquestradorService.__new__(OrquestradorService) service.normalizer = EntityNormalizer() async def fake_extract_sales_search_context_with_llm(message: str, user_id: int | None): return {"orcamento_max": 70000} service._extract_sales_search_context_with_llm = fake_extract_sales_search_context_with_llm result = await service._extract_missing_sales_search_context_with_llm( message="Quero comprar um carro de 70 mil, meu CPF e 12345678909", user_id=7, turn_decision={"domain": "sales", "intent": "order_create", "action": "collect_order_create"}, extracted_entities={ "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {"cpf": "12345678909"}, "cancel_order_fields": {}, "intents": {}, }, ) self.assertEqual(result["orcamento_max"], 70000) async def test_turn_decision_call_tool_executes_without_router(self): service = OrquestradorService.__new__(OrquestradorService) service.tool_executor = FakeToolExecutor(result={"numero_pedido": "PED-1", "status": "Ativo"}) service.llm = FakeLLM([]) service._capture_review_confirmation_suggestion = lambda **kwargs: None service._capture_tool_result_context = lambda **kwargs: None service._should_use_deterministic_response = lambda tool_name: True service._fallback_format_tool_result = lambda tool_name, tool_result: f"{tool_name}:{tool_result['numero_pedido']}" async def fake_render_tool_response_with_fallback(**kwargs): return f"{kwargs['tool_name']}:{kwargs['tool_result']['numero_pedido']}" service._render_tool_response_with_fallback = fake_render_tool_response_with_fallback service._http_exception_detail = lambda exc: str(exc) service._is_low_value_response = lambda text: False async def finish(response: str, queue_notice: str | None = None) -> str: return response if not queue_notice else f"{queue_notice}\n{response}" response = await service._try_execute_business_tool_from_turn_decision( message="quero fechar o pedido", user_id=7, turn_decision={ "action": "call_tool", "tool_name": "realizar_pedido", "tool_arguments": {"cpf": "12345678909", "vehicle_id": 1}, }, queue_notice=None, finish=finish, ) self.assertEqual( service.tool_executor.calls, [("realizar_pedido", {"cpf": "12345678909", "vehicle_id": 1}, 7)], ) self.assertEqual(response, "realizar_pedido:PED-1") self.assertEqual(service.llm.calls, 0) async def test_turn_decision_cancel_order_uses_deterministic_response_without_result_llm(self): service = OrquestradorService.__new__(OrquestradorService) service.tool_executor = FakeToolExecutor(result={"numero_pedido": "PED-1", "status": "Cancelado", "motivo": "desisti"}) service.llm = FakeLLM([]) service._capture_review_confirmation_suggestion = lambda **kwargs: None service._capture_tool_result_context = lambda **kwargs: None service._should_use_deterministic_response = lambda tool_name: tool_name == "cancelar_pedido" service._fallback_format_tool_result = lambda tool_name, tool_result: ( f"Pedido {tool_result['numero_pedido']} atualizado.\nStatus: {tool_result['status']}" ) service._http_exception_detail = lambda exc: str(exc) service._is_low_value_response = lambda text: False async def finish(response: str, queue_notice: str | None = None) -> str: return response if not queue_notice else f"{queue_notice}\n{response}" response = await service._try_execute_business_tool_from_turn_decision( message="cancelar pedido", user_id=7, turn_decision={ "action": "call_tool", "tool_name": "cancelar_pedido", "tool_arguments": {"numero_pedido": "PED-1", "motivo": "desisti"}, }, queue_notice=None, finish=finish, ) self.assertEqual(response, "Pedido PED-1 atualizado.\nStatus: Cancelado") self.assertEqual(service.llm.calls, 0) async def test_turn_decision_rental_fleet_listing_uses_deterministic_response_without_result_llm(self): registry = StaticToolRegistry( result=[ {"id": 1, "placa": "RAA1A01", "modelo": "Chevrolet Tracker", "categoria": "suv", "ano": 2024, "valor_diaria": 219.9, "status": "disponivel"}, {"id": 2, "placa": "RAA1A02", "modelo": "Fiat Pulse", "categoria": "suv", "ano": 2024, "valor_diaria": 189.9, "status": "disponivel"}, ] ) service = OrquestradorService.__new__(OrquestradorService) service.state = FakeState() service.normalizer = EntityNormalizer() service.tool_executor = ToolExecutor(registry=registry) service.llm = FakeLLM([]) service._capture_review_confirmation_suggestion = lambda **kwargs: None service._capture_tool_result_context = lambda **kwargs: None service._capture_tool_invocation_trace = lambda **kwargs: None service._log_turn_event = lambda *args, **kwargs: None async def fake_render_tool_response_with_fallback(**kwargs): raise AssertionError("nao deveria usar llm para listagem de locacao") service._render_tool_response_with_fallback = fake_render_tool_response_with_fallback service._http_exception_detail = lambda exc: str(exc) service._is_low_value_response = lambda text: False async def finish(response: str, queue_notice: str | None = None) -> str: return response if not queue_notice else f"{queue_notice}\n{response}" response = await service._try_execute_business_tool_from_turn_decision( message="quais carros estao disponiveis para aluguel", user_id=7, turn_decision={ "action": "call_tool", "tool_name": "consultar_frota_aluguel", "tool_arguments": {"valor_diaria_max": 220}, }, queue_notice=None, finish=finish, ) self.assertEqual(registry.calls[0][0], "consultar_frota_aluguel") self.assertEqual(registry.calls[0][2], 7) self.assertIn("Encontrei 2 veiculo(s) para locacao:", response) self.assertIn("RAA1A01", response) self.assertIn("numero da lista, a placa ou o modelo", response) self.assertEqual(service.llm.calls, 0) async def test_confirm_pending_review_clears_open_review_draft_after_suggested_time_success(self): state = FakeState( entries={ "pending_review_drafts": { 7: { "payload": { "placa": "ABC1C23", "modelo": "Onix", "ano": 2024, "km": 20000, "revisao_previa_concessionaria": False, }, "expires_at": utc_now() + timedelta(minutes=15), } }, "pending_review_confirmations": { 7: { "payload": { "placa": "ABC1C23", "data_hora": "14/03/2026 16:30", "modelo": "Onix", "ano": 2024, "km": 20000, "revisao_previa_concessionaria": False, }, "expires_at": utc_now() + timedelta(minutes=15), } }, }, contexts={ 7: { "active_domain": "review", "active_task": "review_schedule", "generic_memory": {"placa": "ABC1C23"}, "shared_memory": {"placa": "ABC1C23"}, "collected_slots": { "review_schedule": { "placa": "ABC1C23", "modelo": "Onix", "ano": 2024, } }, "flow_snapshots": { "review_schedule": { "payload": { "placa": "ABC1C23", "modelo": "Onix", "ano": 2024, }, "expires_at": utc_now() + timedelta(minutes=15), }, "review_confirmation": { "payload": { "placa": "ABC1C23", "data_hora": "14/03/2026 16:30", }, "expires_at": utc_now() + timedelta(minutes=15), }, }, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } }, ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.tool_executor = FakeToolExecutor( result={ "protocolo": "REV-TESTE-999", "placa": "ABC1C23", "data_hora": "14/03/2026 16:30", "valor_revisao": 728.0, } ) service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) service._http_exception_detail = lambda exc: str(exc) service._fallback_format_tool_result = lambda tool_name, tool_result: ( f"Revisao agendada com sucesso.\nProtocolo: {tool_result['protocolo']}" ) response = await service._try_confirm_pending_review( message="sim", user_id=7, extracted_review_fields={}, ) self.assertIn("REV-TESTE-999", response) self.assertIsNone(state.get_entry("pending_review_confirmations", 7)) self.assertIsNone(state.get_entry("pending_review_drafts", 7)) self.assertIsNotNone(state.get_entry("last_review_packages", 7)) context = state.get_user_context(7) self.assertEqual(context["active_task"], None) self.assertEqual(context["collected_slots"], {}) self.assertEqual(context["flow_snapshots"], {}) async def test_empty_stock_search_suggests_nearby_options(self): service = OrquestradorService.__new__(OrquestradorService) service.normalizer = EntityNormalizer() service.tool_executor = FakeToolExecutor(result=[]) service._get_user_context = lambda user_id: { "generic_memory": {}, "shared_memory": {}, "last_stock_results": [], "selected_vehicle": None, } service._capture_tool_result_context = lambda tool_name, tool_result, user_id: None service._normalize_positive_number = service.normalizer.normalize_positive_number response = await service._maybe_build_stock_suggestion_response( tool_name="consultar_estoque", arguments={"preco_max": 50000, "limite": 5}, tool_result=[], user_id=5, ) self.assertIn("Nao encontrei veiculos ate R$ 50.000.", response) self.assertIn("Hyundai HB20 2022", response) self.assertIn("Se quiser, responda com o numero da lista ou com o modelo.", response) async def test_turn_decision_answer_user_can_short_circuit_router(self): decision = { "intent": "general", "domain": "general", "action": "answer_user", "response_to_user": "Resposta direta do contrato.", } self.assertEqual(str(decision.get("action") or ""), "answer_user") self.assertEqual(str(decision.get("response_to_user") or "").strip(), "Resposta direta do contrato.") async def test_handle_message_persists_completed_turn_history(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) history_calls = [] service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service.history_service = SimpleNamespace(record_turn=lambda **kwargs: history_calls.append(kwargs)) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "general", "domain": "general", "action": "answer_user", "entities": service.normalizer.empty_extraction_payload(), "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": "Resposta direta do contrato.", } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): return { "orders": [ { "domain": "general", "message": message, "entities": service.normalizer.empty_extraction_payload(), } ] } service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_extract_entities(message: str, user_id: int | None): return service.normalizer.empty_extraction_payload() service._extract_entities_with_llm = fake_extract_entities async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" service._handle_context_switch = lambda **kwargs: None service._update_active_domain = lambda **kwargs: None async def fake_try_execute_orchestration_control_tool(**kwargs): return None service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool async def fake_try_execute_business_tool_from_turn_decision(**kwargs): return None service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision async def fake_try_handle_review_management(**kwargs): return None service._try_handle_review_management = fake_try_handle_review_management async def fake_try_confirm_pending_review(**kwargs): return None service._try_confirm_pending_review = fake_try_confirm_pending_review async def fake_try_collect_and_schedule_review(**kwargs): return None service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def fake_try_collect_and_cancel_order(**kwargs): return None service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order async def fake_try_handle_order_listing(**kwargs): return None service._try_handle_order_listing = fake_try_handle_order_listing async def fake_try_collect_and_create_order(**kwargs): return None service._try_collect_and_create_order = fake_try_collect_and_create_order response = await service.handle_message( "ola", user_id=1, ) self.assertEqual(response, "Resposta direta do contrato.") self.assertEqual(len(history_calls), 1) self.assertEqual(history_calls[0]["user_message"], "ola") self.assertEqual(history_calls[0]["assistant_response"], "Resposta direta do contrato.") self.assertEqual(history_calls[0]["turn_status"], "completed") self.assertEqual(history_calls[0]["intent"], "general") async def test_handle_message_restores_outer_turn_trace_after_nested_call(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) history_calls = [] service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service.history_service = SimpleNamespace(record_turn=lambda **kwargs: history_calls.append(kwargs)) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._upsert_user_context = lambda user_id: None service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): if base_response == "resposta externa": nested_response = await service.handle_message("mensagem interna", user_id=user_id) return f"{base_response}\n{nested_response}" return base_response async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "general", "domain": "general", "action": "answer_user", "entities": service.normalizer.empty_extraction_payload(), "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": "resposta interna" if message == "mensagem interna" else "resposta externa", } async def fake_extract_message_plan(message: str, user_id: int | None): return {"orders": [{"domain": "general", "message": message}]} service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._extract_turn_decision_with_llm = fake_extract_turn_decision service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_try_handle_immediate_context_reset(**kwargs): return None async def fake_try_resolve_pending_order_selection(**kwargs): return None async def fake_try_continue_queued_order(**kwargs): return None async def fake_try_execute_orchestration_control_tool(**kwargs): return None async def fake_try_execute_business_tool_from_turn_decision(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection service._try_continue_queued_order = fake_try_continue_queued_order service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision service._handle_context_switch = lambda **kwargs: None service._update_active_domain = lambda **kwargs: None async def fake_extract_entities_with_llm(message: str, user_id: int | None): return service.normalizer.empty_extraction_payload() async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_entities_with_llm = fake_extract_entities_with_llm service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" response = await service.handle_message("mensagem externa", user_id=1) self.assertEqual(response, "resposta externa\nresposta interna") self.assertEqual(len(history_calls), 2) self.assertEqual( {call["user_message"] for call in history_calls}, {"mensagem externa", "mensagem interna"}, ) self.assertEqual( {call["assistant_response"] for call in history_calls}, {"resposta externa\nresposta interna", "resposta interna"}, ) self.assertEqual(len({call["request_id"] for call in history_calls}), 2) async def test_handle_message_persists_failed_turn_history(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) history_calls = [] service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service.history_service = SimpleNamespace(record_turn=lambda **kwargs: history_calls.append(kwargs)) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): raise RuntimeError("falha controlada no turno") service._extract_turn_decision_with_llm = fake_extract_turn_decision with self.assertRaises(RuntimeError): await service.handle_message( "ola", user_id=1, ) self.assertEqual(len(history_calls), 1) self.assertEqual(history_calls[0]["user_message"], "ola") self.assertIsNone(history_calls[0]["assistant_response"]) self.assertEqual(history_calls[0]["turn_status"], "failed") self.assertIn("RuntimeError", history_calls[0]["error_detail"]) self.assertIn("falha controlada no turno", history_calls[0]["error_detail"]) def test_log_turn_event_masks_sensitive_payload(self): service = OrquestradorService.__new__(OrquestradorService) service._turn_trace = { "request_id": "req-1", "conversation_id": "user:7", } with patch("app.services.orchestration.orquestrador_service.logger.info") as logger_info: service._log_turn_event( "tool_completed", message="Meu cpf 12345678909 e a placa ABC1D23.", arguments={ "cpf": "12345678909", "placa": "ABC1D23", "external_id": "987654321", "identificador_comprovante": "NSU123", }, result={ "placa": "ABC1D23", "identificador_comprovante": "NSU123", }, ) self.assertTrue(logger_info.called) payload = logger_info.call_args.args[2] payload_text = str(payload) self.assertNotIn("12345678909", payload_text) self.assertNotIn("ABC1D23", payload_text) self.assertNotIn("987654321", payload_text) self.assertNotIn("NSU123", payload_text) self.assertIn("***.***.***-09", payload_text) self.assertIn("ABC***3", payload_text) self.assertIn("******321", payload_text) self.assertIn("***123", payload_text) async def test_handle_message_prioritizes_order_flow_over_model_answer_for_purchase_intent(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "order_create", "domain": "sales", "action": "answer_user", "entities": { "generic_memory": {"cpf": "12345678909", "orcamento_max": 70000}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": "Certo! Para te ajudar a encontrar o carro ideal dentro do seu orcamento.", } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): return { "orders": [ { "domain": "sales", "message": message, "entities": service.normalizer.empty_extraction_payload(), } ] } service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_extract_entities(message: str, user_id: int | None): return { "generic_memory": {"cpf": "12345678909", "orcamento_max": 70000}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, } service._extract_entities_with_llm = fake_extract_entities async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" service._handle_context_switch = lambda **kwargs: None service._update_active_domain = lambda **kwargs: None async def fake_try_execute_orchestration_control_tool(**kwargs): return None service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool async def fake_try_execute_business_tool_from_turn_decision(**kwargs): return None service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision async def fake_try_handle_review_management(**kwargs): return None service._try_handle_review_management = fake_try_handle_review_management async def fake_try_confirm_pending_review(**kwargs): return None service._try_confirm_pending_review = fake_try_confirm_pending_review async def fake_try_collect_and_schedule_review(**kwargs): return None service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def fake_try_collect_and_cancel_order(**kwargs): return None service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order async def fake_try_collect_and_create_order(**kwargs): return "Encontrei 2 veiculo(s):\n1. Hyundai HB20 2022" service._try_collect_and_create_order = fake_try_collect_and_create_order response = await service.handle_message( "Quero comprar um carro de 70 mil, meu CPF e 12345678909", user_id=1, ) self.assertIn("Encontrei 2 veiculo(s):", response) async def test_handle_message_prioritizes_order_flow_for_explicit_order_request_without_extracted_fields(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "general", "domain": "general", "action": "answer_user", "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": "Claro! Voce gostaria de fazer um pedido de um veiculo ou agendar uma revisao?", } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): return { "orders": [ { "domain": "sales", "message": message, "entities": service.normalizer.empty_extraction_payload(), } ] } service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_extract_entities(message: str, user_id: int | None): return { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, } service._extract_entities_with_llm = fake_extract_entities async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" service._handle_context_switch = lambda **kwargs: None service._update_active_domain = lambda **kwargs: None async def fake_try_execute_orchestration_control_tool(**kwargs): return None service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool async def fake_try_execute_business_tool_from_turn_decision(**kwargs): return None service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision async def fake_try_handle_review_management(**kwargs): return None service._try_handle_review_management = fake_try_handle_review_management async def fake_try_confirm_pending_review(**kwargs): return None service._try_confirm_pending_review = fake_try_confirm_pending_review async def fake_try_collect_and_schedule_review(**kwargs): return None service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def fake_try_collect_and_cancel_order(**kwargs): return None service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order async def fake_try_handle_order_listing(**kwargs): return None service._try_handle_order_listing = fake_try_handle_order_listing async def fake_try_collect_and_create_order(**kwargs): return ( "Para seguir com o pedido, me diga qual carro voce procura.\n" "Se preferir, posso listar opcoes por faixa de preco, modelo ou tipo de carro." ) service._try_collect_and_create_order = fake_try_collect_and_create_order response = await service.handle_message( "Quero fazer um pedido", user_id=1, ) self.assertIn("Para seguir com o pedido", response) self.assertNotIn("Qual veiculo voce gostaria de pedir", response) def test_should_prioritize_review_flow_when_review_draft_is_open(self): state = FakeState( entries={ "pending_review_drafts": { 1: { "payload": {"placa": "ABC1269"}, "expires_at": utc_now() + timedelta(minutes=15), } } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() prioritized = service._should_prioritize_review_flow( turn_decision={"intent": "general", "domain": "general", "action": "answer_user"}, extracted_entities={ "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, }, user_id=1, ) self.assertTrue(prioritized) def test_should_prioritize_review_flow_when_active_domain_is_review(self): state = FakeState( contexts={ 1: { "active_domain": "review", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "expires_at": utc_now() + timedelta(minutes=15), } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() prioritized = service._should_prioritize_review_flow( turn_decision={"intent": "general", "domain": "general", "action": "answer_user"}, extracted_entities={ "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, }, user_id=1, ) self.assertTrue(prioritized) def test_should_prioritize_review_flow_for_review_schedule_intent_without_prefilled_fields(self): service = OrquestradorService.__new__(OrquestradorService) service.state = FakeState() service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: None prioritized = service._should_prioritize_review_flow( turn_decision={"intent": "review_schedule", "domain": "review", "action": "ask_missing_fields"}, extracted_entities={ "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, }, user_id=1, ) self.assertTrue(prioritized) async def test_review_schedule_direct_flow_captures_email_side_effects_after_success(self): state = FakeState( entries={ "pending_review_drafts": { 1: { "payload": { "placa": "ABC1D23", "data_hora": "2026-03-25T10:00:00", "modelo": "Onix", "ano": 2022, "km": 35000, "revisao_previa_concessionaria": False, }, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "review", "active_task": "review_schedule", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "expires_at": utc_now() + timedelta(minutes=15), } }, ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.tool_executor = FakeToolExecutor( result={ "protocolo": "REV-20260325-084279F3", "placa": "ABC1D23", "data_hora": "2026-03-25T10:00:00", "modelo": "Onix", "ano": 2022, "km": 35000, "valor_revisao": 906.0, "status": "agendado", "user_id": 1, } ) service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) service._try_prefill_review_fields_from_memory = lambda user_id, payload: None service._store_last_review_package = lambda user_id, payload: None service._log_review_flow_source = lambda **kwargs: None service._fallback_format_tool_result = lambda tool_name, tool_result: "Revisao agendada com sucesso." captured = [] service._capture_successful_tool_side_effects = lambda **kwargs: captured.append(kwargs) response = await service._try_collect_and_schedule_review( message="ok", user_id=1, extracted_fields={}, intents={}, turn_decision={"intent": "review_schedule", "domain": "review", "action": "collect_review_schedule"}, ) self.assertEqual(response, "Revisao agendada com sucesso.") self.assertEqual(len(captured), 1) self.assertEqual(captured[0]["tool_name"], "agendar_revisao") self.assertEqual(captured[0]["user_id"], 1) self.assertEqual(captured[0]["tool_result"]["protocolo"], "REV-20260325-084279F3") async def test_handle_message_prioritizes_review_management_over_model_answer_for_reschedule_intent(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "review_reschedule", "domain": "review", "action": "answer_user", "entities": service.normalizer.empty_extraction_payload(), "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": "Claro, para qual data e horario voce gostaria de remarcar?", } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): return { "orders": [ { "domain": "review", "message": message, "entities": service.normalizer.empty_extraction_payload(), } ] } service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_extract_entities(message: str, user_id: int | None): return service.normalizer.empty_extraction_payload() service._extract_entities_with_llm = fake_extract_entities async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" service._handle_context_switch = lambda **kwargs: None service._update_active_domain = lambda **kwargs: None async def fake_try_execute_orchestration_control_tool(**kwargs): return None service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool async def fake_try_execute_business_tool_from_turn_decision(**kwargs): return None service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision async def fake_try_handle_review_management(**kwargs): return "Para remarcar sua revisao, preciso dos dados abaixo:\n- a nova data e hora desejada para a revisao" service._try_handle_review_management = fake_try_handle_review_management async def fake_try_confirm_pending_review(**kwargs): return None service._try_confirm_pending_review = fake_try_confirm_pending_review async def fake_try_collect_and_schedule_review(**kwargs): return None service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def fake_try_collect_and_cancel_order(**kwargs): return None service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order async def fake_try_handle_order_listing(**kwargs): return None service._try_handle_order_listing = fake_try_handle_order_listing async def fake_try_collect_and_create_order(**kwargs): return None service._try_collect_and_create_order = fake_try_collect_and_create_order response = await service.handle_message( "quero remarcar o meu agendamento REV-20260317-54E9D3CB", user_id=1, ) self.assertIn("a nova data e hora desejada", response) async def test_handle_message_prioritizes_review_flow_over_model_answer_for_followup(self): state = FakeState( entries={ "pending_review_drafts": { 1: { "payload": {"placa": "ABC1269"}, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "review", "generic_memory": {"placa": "ABC1269"}, "shared_memory": {"placa": "ABC1269"}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } }, ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "general", "domain": "general", "action": "answer_user", "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": "Para que tipo de compromisso voce gostaria de marcar amanha as 16 horas?", } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): return { "orders": [ { "domain": "review", "message": message, "entities": service.normalizer.empty_extraction_payload(), } ] } service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_extract_entities(message: str, user_id: int | None): return { "generic_memory": {}, "review_fields": {"data_hora": "13/03/2026 16:00"}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, } service._extract_entities_with_llm = fake_extract_entities async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" service._handle_context_switch = lambda **kwargs: None service._update_active_domain = lambda **kwargs: None async def fake_try_execute_orchestration_control_tool(**kwargs): return None service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool async def fake_try_execute_business_tool_from_turn_decision(**kwargs): return None service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision async def fake_try_handle_review_management(**kwargs): return None service._try_handle_review_management = fake_try_handle_review_management async def fake_try_confirm_pending_review(**kwargs): return None service._try_confirm_pending_review = fake_try_confirm_pending_review async def fake_try_collect_and_schedule_review(**kwargs): return "Para agendar sua revisao, preciso dos dados abaixo:\n- o modelo do veiculo" service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def fake_try_collect_and_cancel_order(**kwargs): return None service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order async def fake_try_handle_order_listing(**kwargs): return None service._try_handle_order_listing = fake_try_handle_order_listing async def fake_try_collect_and_create_order(**kwargs): return None service._try_collect_and_create_order = fake_try_collect_and_create_order response = await service.handle_message( "Eu gostaria de marcar amanha as 16 horas", user_id=1, ) self.assertIn("o modelo do veiculo", response) async def test_handle_message_skips_inventory_tool_and_uses_order_flow_for_list_selection_follow_up(self): state = FakeState( contexts={ 1: { "active_domain": "sales", "generic_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "shared_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [ {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0}, {"id": 11, "modelo": "Toyota Corolla 2024", "categoria": "suv", "preco": 76087.0}, ], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "inventory_search", "domain": "sales", "action": "call_tool", "entities": { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, }, "missing_fields": [], "selection_index": None, "tool_name": "consultar_estoque", "tool_arguments": {"preco_max": 70000, "categoria": "suv"}, "response_to_user": "", } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): return { "orders": [ { "domain": "sales", "message": message, "entities": service.normalizer.empty_extraction_payload(), } ] } service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_extract_entities(message: str, user_id: int | None): return { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, } service._extract_entities_with_llm = fake_extract_entities async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" service._handle_context_switch = lambda **kwargs: None service._update_active_domain = lambda **kwargs: None async def fake_try_execute_orchestration_control_tool(**kwargs): return None service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool async def fake_try_execute_business_tool_from_turn_decision(**kwargs): return "nao deveria chamar tool" service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision async def fake_try_handle_review_management(**kwargs): return None service._try_handle_review_management = fake_try_handle_review_management async def fake_try_confirm_pending_review(**kwargs): return None service._try_confirm_pending_review = fake_try_confirm_pending_review async def fake_try_collect_and_schedule_review(**kwargs): return None service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def fake_try_collect_and_cancel_order(**kwargs): return None service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order async def fake_try_handle_order_listing(**kwargs): return None service._try_handle_order_listing = fake_try_handle_order_listing async def fake_try_collect_and_create_order(**kwargs): return "Para realizar o pedido, preciso dos dados abaixo:\n- o CPF do cliente" service._try_collect_and_create_order = fake_try_collect_and_create_order response = await service.handle_message( "quero a opcao 1", user_id=1, ) self.assertIn("CPF do cliente", response) async def test_handle_message_short_circuits_llm_when_pending_stock_selection_matches_list_choice(self): state = FakeState( entries={ "pending_stock_selections": { 1: { "payload": [ {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0}, {"id": 11, "modelo": "Toyota Corolla 2024", "categoria": "suv", "preco": 76087.0}, ], "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "sales", "generic_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "shared_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o LLM para selecao pendente de estoque") service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_try_collect_and_create_order(**kwargs): return "Para realizar o pedido, preciso dos dados abaixo:\n- o CPF do cliente" service._try_collect_and_create_order = fake_try_collect_and_create_order response = await service.handle_message( "quero a opcao 1", user_id=1, ) self.assertIn("CPF do cliente", response) async def test_handle_message_short_circuits_llm_when_pending_rental_selection_matches_list_choice(self): state = FakeState( entries={ "pending_rental_selections": { 1: { "payload": [ {"id": 1, "placa": "RAA1A01", "modelo": "Chevrolet Tracker", "categoria": "suv", "ano": 2024, "valor_diaria": 219.9, "status": "disponivel"}, {"id": 2, "placa": "RAA1A02", "modelo": "Fiat Pulse", "categoria": "suv", "ano": 2024, "valor_diaria": 189.9, "status": "disponivel"}, ], "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "rental", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o LLM para selecao pendente de locacao") service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_try_collect_and_open_rental(**kwargs): return "Para abrir a locacao, preciso dos dados abaixo:\n- a data e hora de inicio da locacao" service._try_collect_and_open_rental = fake_try_collect_and_open_rental response = await service.handle_message( "1", user_id=1, ) self.assertIn("inicio da locacao", response) async def test_handle_message_keeps_rental_create_flow_when_user_informs_due_date_with_devolucao_label(self): state = FakeState( entries={ "pending_rental_drafts": { 1: { "payload": { "rental_vehicle_id": 3, "placa": "RAA1A02", "data_inicio": "19/03/2026 10:00", }, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "rental", "active_task": "rental_create", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": {"id": 3, "placa": "RAA1A02", "modelo": "Fiat Pulse"}, "last_rental_contract": { "contrato_numero": "LOC-20260319-33CD6567", "placa": "RAA1A02", }, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o LLM durante follow-up ativo de locacao") service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_try_collect_and_open_rental(**kwargs): self.assertEqual(kwargs["message"], "devolucao 21/03/2026 10:00") return "locacao aberta" service._try_collect_and_open_rental = fake_try_collect_and_open_rental response = await service.handle_message( "devolucao 21/03/2026 10:00", user_id=1, ) self.assertEqual(response, "locacao aberta") async def test_handle_message_short_circuits_for_rental_return_using_last_contract(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": None, "last_rental_contract": { "contrato_numero": "LOC-20260318-FE69BCF0", "placa": "RAA1A12", }, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service.tool_executor = FakeToolExecutor( result={ "contrato_numero": "LOC-20260318-FE69BCF0", "placa": "RAA1A12", "modelo_veiculo": "Peugeot 208", "data_devolucao": "2026-03-18T15:46:00", "valor_final": 449.7, } ) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._fallback_format_tool_result = lambda tool_name, tool_result: "devolucao ok" service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "general", "domain": "general", "action": "answer_user", "entities": service.normalizer.empty_extraction_payload(), "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": None, } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o planner para devolucao deterministica de aluguel") service._extract_message_plan_with_llm = fake_extract_message_plan response = await service.handle_message( "devolver a placa RAA1A12", user_id=1, ) self.assertTrue(response.startswith("devolucao ok")) self.assertIn("Se quiser, posso te enviar esse resumo por e-mail.", response) self.assertEqual( service.tool_executor.calls, [ ( "registrar_devolucao_aluguel", {"placa": "RAA1A12", "contrato_numero": "LOC-20260318-FE69BCF0"}, 1, ) ], ) async def test_handle_message_short_circuits_for_rental_payment_receipt_text(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": None, "last_rental_contract": { "contrato_numero": "LOC-20260318-FE69BCF0", "placa": "RAA1A12", }, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service.tool_executor = FakeToolExecutor( result={ "protocolo": "ALP-20260318-ABCD1234", "contrato_numero": "LOC-20260318-FE69BCF0", "placa": "RAA1A12", "valor": 449.7, "data_pagamento": "2026-03-18T15:47:00", } ) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._fallback_format_tool_result = lambda tool_name, tool_result: "pagamento ok" service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "general", "domain": "general", "action": "answer_user", "entities": service.normalizer.empty_extraction_payload(), "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": None, } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o planner para pagamento deterministico de aluguel") service._extract_message_plan_with_llm = fake_extract_message_plan response = await service.handle_message( "Registrar pagamento de aluguel: valor 449,70; data_pagamento 18/03/2026 15:47; favorecido Locadora XPTO; identificador_comprovante NSU123.", user_id=1, ) self.assertTrue(response.startswith("pagamento ok")) self.assertIn("Se quiser, posso te enviar esse resumo por e-mail.", response) self.assertEqual( service.tool_executor.calls, [ ( "registrar_pagamento_aluguel", { "valor": 449.7, "data_pagamento": "18/03/2026 15:47", "favorecido": "Locadora XPTO", "identificador_comprovante": "NSU123", "contrato_numero": "LOC-20260318-FE69BCF0", "placa": "RAA1A12", }, 1, ) ], ) async def test_handle_message_short_circuits_for_rental_payment_receipt_text_with_angle_brackets(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": None, "last_rental_contract": { "contrato_numero": "LOC-20260318-4B85490F", "placa": "RAA1A22", }, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service.tool_executor = FakeToolExecutor( result={ "protocolo": "ALP-20260318-ABCD1234", "contrato_numero": "LOC-20260318-4B85490F", "placa": "RAA1A22", "valor": 479.7, "data_pagamento": "2026-03-18T16:10:00", } ) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._fallback_format_tool_result = lambda tool_name, tool_result: "pagamento ok" service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "general", "domain": "general", "action": "answer_user", "entities": service.normalizer.empty_extraction_payload(), "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": None, } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o planner para pagamento deterministico de aluguel") service._extract_message_plan_with_llm = fake_extract_message_plan response = await service.handle_message( "[imagem recebida no telegram]\nDados extraidos da imagem: Registrar pagamento de aluguel: contrato ; placa ; valor ; data_pagamento <18/03/2026 16:10>; favorecido ; identificador_comprovante ; observacoes .", user_id=1, ) self.assertTrue(response.startswith("pagamento ok")) self.assertIn("Se quiser, posso te enviar esse resumo por e-mail.", response) self.assertEqual( service.tool_executor.calls, [ ( "registrar_pagamento_aluguel", { "contrato_numero": "LOC-20260318-4B85490F", "valor": 479.7, "data_pagamento": "18/03/2026 16:10", "favorecido": "Locadora XPTO", "identificador_comprovante": "NSU123456", "observacoes": "pagamento da locacao", "placa": "RAA1A22", }, 1, ) ], ) async def test_handle_message_short_circuits_for_current_rental_info_question(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": None, "last_rental_contract": { "contrato_numero": "LOC-20260323-CAEECA1C", "placa": "RAA1A02", "modelo_veiculo": "Fiat Pulse", "data_inicio": "2026-03-19T10:00:00", "data_fim_prevista": "2026-03-21T10:00:00", "valor_diaria": 189.9, "valor_previsto": 379.8, "status": "ativa", "status_pagamento": "registrado", "data_pagamento": "2026-03-23T15:47:00", "valor_pagamento": 379.8, }, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_try_handle_pending_stock_selection_follow_up(**kwargs): return None async def fake_try_handle_active_sales_follow_up(**kwargs): return None async def fake_try_handle_pending_rental_selection_follow_up(**kwargs): return None async def fake_try_handle_active_rental_follow_up(**kwargs): return None async def fake_try_handle_active_review_follow_up(**kwargs): return None service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up service._try_handle_pending_rental_selection_follow_up = fake_try_handle_pending_rental_selection_follow_up service._try_handle_active_rental_follow_up = fake_try_handle_active_rental_follow_up service._try_handle_active_review_follow_up = fake_try_handle_active_review_follow_up async def fake_extract_turn_decision(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o LLM para consulta informativa do aluguel atual") service._extract_turn_decision_with_llm = fake_extract_turn_decision response = await service.handle_message( "qual a data de devolucao do meu aluguel?", user_id=1, ) self.assertIn("A devolucao prevista do seu aluguel e 21/03/2026 10:00.", response) self.assertIn("Contrato: LOC-20260323-CAEECA1C", response) self.assertIn("Veiculo: Fiat Pulse", response) async def test_handle_message_rehydrates_current_rental_info_from_db_after_restart(self): engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) MockBase.metadata.create_all(bind=engine) self.addCleanup(engine.dispose) db = SessionLocal() try: vehicle = RentalVehicle( placa="RAA1A02", modelo="Fiat Pulse", categoria="suv", ano=2024, valor_diaria=189.9, status="disponivel", ) db.add(vehicle) db.commit() db.refresh(vehicle) contract = RentalContract( contrato_numero="LOC-20260323-CAEECA1C", user_id=1, rental_vehicle_id=vehicle.id, placa=vehicle.placa, modelo_veiculo=vehicle.modelo, categoria=vehicle.categoria, data_inicio=datetime(2026, 3, 19, 10, 0), data_fim_prevista=datetime(2026, 3, 21, 10, 0), data_devolucao=None, valor_diaria=189.9, valor_previsto=379.8, valor_final=None, status="ativa", ) db.add(contract) db.commit() db.refresh(contract) payment = RentalPayment( protocolo="ALP-20260323-0B41DD0D", user_id=1, rental_contract_id=contract.id, contrato_numero=contract.contrato_numero, placa=contract.placa, valor=379.8, data_pagamento=datetime(2026, 3, 23, 15, 47), favorecido="Locadora XPTO", identificador_comprovante="NSU123456", observacoes="pagamento da locacao", ) db.add(payment) db.commit() finally: db.close() state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_try_handle_pending_stock_selection_follow_up(**kwargs): return None async def fake_try_handle_active_sales_follow_up(**kwargs): return None async def fake_try_handle_pending_rental_selection_follow_up(**kwargs): return None async def fake_try_handle_active_rental_follow_up(**kwargs): return None async def fake_try_handle_active_review_follow_up(**kwargs): return None service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up service._try_handle_pending_rental_selection_follow_up = fake_try_handle_pending_rental_selection_follow_up service._try_handle_active_rental_follow_up = fake_try_handle_active_rental_follow_up service._try_handle_active_review_follow_up = fake_try_handle_active_review_follow_up async def fake_extract_turn_decision(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o LLM para consulta informativa do aluguel apos restart") service._extract_turn_decision_with_llm = fake_extract_turn_decision with patch("app.services.flows.rental_flow_support.SessionMockLocal", SessionLocal): response = await service.handle_message( "qual a data de devolucao do meu aluguel?", user_id=1, ) self.assertIn("A devolucao prevista do seu aluguel e 21/03/2026 10:00.", response) self.assertIn("Contrato: LOC-20260323-CAEECA1C", response) self.assertIn("Veiculo: Fiat Pulse", response) snapshot = state.get_user_context(1)["last_rental_contract"] self.assertEqual(snapshot["contrato_numero"], "LOC-20260323-CAEECA1C") self.assertEqual(snapshot["status_pagamento"], "registrado") self.assertEqual(snapshot["data_fim_prevista"], "2026-03-21T10:00:00") def test_store_last_rental_contract_preserves_contract_snapshot_after_payment_update(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) service._store_last_rental_contract( user_id=1, payload={ "contrato_numero": "LOC-20260323-CAEECA1C", "placa": "RAA1A02", "modelo_veiculo": "Fiat Pulse", "data_inicio": "2026-03-19T10:00:00", "data_fim_prevista": "2026-03-21T10:00:00", "valor_diaria": 189.9, "valor_previsto": 379.8, "status": "ativa", }, ) service._store_last_rental_contract( user_id=1, payload={ "contrato_numero": "LOC-20260323-CAEECA1C", "placa": "RAA1A02", "valor": 379.8, "data_pagamento": "2026-03-23T15:47:00", "favorecido": "Locadora XPTO", "status": "registrado", }, ) snapshot = state.get_user_context(1)["last_rental_contract"] self.assertEqual(snapshot["modelo_veiculo"], "Fiat Pulse") self.assertEqual(snapshot["data_fim_prevista"], "2026-03-21T10:00:00") self.assertEqual(snapshot["status"], "ativa") self.assertEqual(snapshot["status_pagamento"], "registrado") self.assertEqual(snapshot["data_pagamento"], "2026-03-23T15:47:00") self.assertEqual(snapshot["valor_pagamento"], 379.8) def test_has_rental_return_management_request_ignores_return_question_even_with_last_contract(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": None, "last_rental_contract": { "contrato_numero": "LOC-20260318-FE69BCF0", "placa": "RAA1A12", }, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) self.assertFalse( service._has_rental_return_management_request( "qual a data de devolucao do meu aluguel?", user_id=1, ) ) def test_has_rental_payment_request_requires_current_rental_reference(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, "last_rental_results": [], "selected_rental_vehicle": None, "last_rental_contract": { "contrato_numero": "LOC-20260318-FE69BCF0", "placa": "RAA1A12", }, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) self.assertFalse(service._has_rental_payment_request("segue comprovante pix de R$ 500", user_id=1)) self.assertTrue(service._has_rental_payment_request("segue comprovante do aluguel de R$ 500", user_id=1)) async def test_handle_message_keeps_sales_flow_when_cpf_follow_up_is_misclassified_as_review(self): state = FakeState( entries={ "pending_order_drafts": { 1: { "payload": {"vehicle_id": 15, "modelo_veiculo": "Volkswagen T-Cross 2022", "valor_veiculo": 73224.0}, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "sales", "generic_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "shared_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [ {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True}, ], "selected_vehicle": {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True}, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_extract_turn_decision(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o LLM para continuar um fluxo de venda aberto") service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_message_plan(message: str, user_id: int | None): return { "orders": [ { "domain": "sales", "message": message, "entities": service.normalizer.empty_extraction_payload(), } ] } service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_extract_entities(message: str, user_id: int | None): return { "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, } service._extract_entities_with_llm = fake_extract_entities async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" service._update_active_domain = lambda **kwargs: None async def fake_try_execute_orchestration_control_tool(**kwargs): return None service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool async def fake_try_execute_business_tool_from_turn_decision(**kwargs): return "nao deveria executar tool planejada" service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision async def fake_try_handle_review_management(**kwargs): return None service._try_handle_review_management = fake_try_handle_review_management async def fake_try_confirm_pending_review(**kwargs): return None service._try_confirm_pending_review = fake_try_confirm_pending_review async def fake_try_collect_and_schedule_review(**kwargs): return None service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def fake_try_collect_and_cancel_order(**kwargs): return None service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order async def fake_try_handle_order_listing(**kwargs): return None service._try_handle_order_listing = fake_try_handle_order_listing async def fake_try_collect_and_create_order(**kwargs): return "Pedido criado com sucesso." service._try_collect_and_create_order = fake_try_collect_and_create_order response = await service.handle_message( "12345678909", user_id=1, ) self.assertEqual(response, "Pedido criado com sucesso.") async def test_handle_message_short_circuits_active_review_time_follow_up_before_llm(self): state = FakeState( entries={ "pending_review_drafts": { 1: { "payload": { "placa": "ABC1234", "data_hora_base": "17/03/2026", }, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "review", "generic_memory": {"placa": "ABC1234"}, "shared_memory": {"placa": "ABC1234"}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_try_handle_pending_stock_selection_follow_up(**kwargs): return None service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up async def fake_extract_turn_decision(message: str, user_id: int | None): raise AssertionError("nao deveria consultar o LLM para um follow-up temporal de revisao com draft aberto") service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_try_collect_and_schedule_review(**kwargs): self.assertEqual(kwargs["turn_decision"]["intent"], "review_schedule") return "Para agendar sua revisao, preciso dos dados abaixo:\n- o modelo do veiculo" service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def fake_try_handle_review_management(**kwargs): return None service._try_handle_review_management = fake_try_handle_review_management async def fake_try_confirm_pending_review(**kwargs): return None service._try_confirm_pending_review = fake_try_confirm_pending_review response = await service.handle_message( "15h", user_id=1, ) self.assertIn("o modelo do veiculo", response) async def test_handle_message_allows_explicit_sales_shift_before_active_review_follow_up_short_circuit(self): state = FakeState( entries={ "pending_review_drafts": { 1: { "payload": { "placa": "ABC1234", "data_hora_base": "17/03/2026", }, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "review", "generic_memory": {"placa": "ABC1234"}, "shared_memory": {"placa": "ABC1234"}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_try_handle_pending_stock_selection_follow_up(**kwargs): return None service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "order_create", "domain": "sales", "action": "collect_order_create", "entities": service.normalizer.empty_extraction_payload(), "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": "", } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_extract_message_plan(message: str, user_id: int | None): return { "orders": [ { "domain": "sales", "message": message, "entities": service.normalizer.empty_extraction_payload(), } ] } service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_extract_entities(message: str, user_id: int | None): return service.normalizer.empty_extraction_payload() service._extract_entities_with_llm = fake_extract_entities async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" service._update_active_domain = lambda **kwargs: None service._handle_context_switch = lambda **kwargs: "Entendi que voce quer sair de agendamento de revisao e ir para compra de veiculo. Tem certeza?" async def fake_try_collect_and_schedule_review(**kwargs): raise AssertionError("nao deveria consumir uma solicitacao explicita de compra como follow-up de revisao") service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review response = await service.handle_message( "quero comprar um carro ate 80 mil", user_id=1, ) self.assertEqual( response, "Entendi que voce quer sair de agendamento de revisao e ir para compra de veiculo. Tem certeza?", ) async def test_handle_message_prioritizes_pending_switch_confirmation_before_sales_follow_up(self): state = FakeState( entries={ "pending_stock_selections": { 1: { "payload": [ {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0}, ], "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "sales", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": { "source_domain": "sales", "target_domain": "review", "expires_at": utc_now() + timedelta(minutes=15), }, "last_stock_results": [ {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0}, ], "selected_vehicle": {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0}, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None service._is_order_selection_reset_message = lambda message: False async def fake_try_handle_pending_stock_selection_follow_up(**kwargs): raise AssertionError("nao deveria entrar no follow-up de estoque antes de confirmar a troca de contexto") service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up async def fake_try_handle_active_sales_follow_up(**kwargs): raise AssertionError("nao deveria entrar no follow-up de vendas antes de confirmar a troca de contexto") service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up response = await service.handle_message( "sim", user_id=1, ) self.assertEqual( response, "Certo, contexto anterior encerrado. Vamos seguir com agendamento de revisao.\n" "Pode me informar a placa ou, se preferir, ja mandar placa, data/hora, modelo, ano, km e se ja fez revisao.", ) self.assertEqual(state.get_user_context(1)["active_domain"], "review") self.assertIsNone(state.get_user_context(1).get("pending_switch")) self.assertIsNone(state.get_entry("pending_stock_selections", 1)) async def test_handle_message_prioritizes_immediate_reset_before_active_sales_follow_up(self): state = FakeState( contexts={ 1: { "active_domain": "sales", "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None service._is_order_selection_reset_message = lambda message: True async def fake_try_handle_immediate_context_reset(**kwargs): return "Contexto da conversa limpo. Podemos recomecar do zero." service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_handle_pending_stock_selection_follow_up(**kwargs): raise AssertionError("nao deveria entrar no follow-up de estoque antes do reset") service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up async def fake_try_handle_active_sales_follow_up(**kwargs): raise AssertionError("nao deveria entrar no follow-up de vendas antes do reset") service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up response = await service.handle_message( "esqueca tudo e vamos recomecar", user_id=1, ) self.assertEqual(response, "Contexto da conversa limpo. Podemos recomecar do zero.") async def test_try_handle_immediate_context_reset_treats_vamos_recomecar_suffix_as_pure_reset(self): state = FakeState( contexts={ 1: { "active_domain": "sales", "generic_memory": {"orcamento_max": 65000}, "shared_memory": {"orcamento_max": 65000}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [{"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0}], "selected_vehicle": {"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0}, "last_rental_contract": { "contrato_numero": "LOC-20260319-33CD6567", "placa": "RAA1A02", }, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def finish(response: str, queue_notice: str | None = None): return response async def fake_handle_message(message: str, user_id: int | None = None): raise AssertionError("nao deveria reprocessar 'vamos recomecar' como mensagem de negocio") service.handle_message = fake_handle_message response = await service._try_handle_immediate_context_reset( message="esqueca tudo e vamos recomecar", user_id=1, turn_decision={"action": "clear_context"}, finish=finish, ) self.assertEqual(response, "Contexto da conversa limpo. Podemos recomecar do zero.") self.assertEqual(state.get_user_context(1)["active_domain"], "general") self.assertEqual(state.get_user_context(1)["generic_memory"], {}) self.assertIsNone(state.get_user_context(1).get("last_rental_contract")) async def test_active_sales_follow_up_ignores_order_listing_request_with_open_order_draft(self): state = FakeState( entries={ "pending_order_drafts": { 1: { "payload": { "cpf": "12345678909", "vehicle_id": 15, "modelo_veiculo": "Volkswagen T-Cross 2022", "valor_veiculo": 73224.0, }, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "sales", "generic_memory": {"cpf": "12345678909"}, "shared_memory": {"cpf": "12345678909"}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [ {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True}, ], "selected_vehicle": {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True}, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_try_collect_and_create_order(**kwargs): raise AssertionError("nao deveria consumir listagem de pedidos como continuacao de order_create") service._try_collect_and_create_order = fake_try_collect_and_create_order async def finish(response: str, queue_notice: str | None = None): return response response = await service._try_handle_active_sales_follow_up( message="Liste os meus pedidos", user_id=1, finish=finish, ) self.assertIsNone(response) self.assertIsNotNone(state.get_entry("pending_order_drafts", 1)) async def test_active_sales_follow_up_prioritizes_cancel_order_over_stale_order_create_draft(self): state = FakeState( entries={ "pending_order_drafts": { 1: { "payload": { "cpf": "12345678909", "vehicle_id": 15, "modelo_veiculo": "Volkswagen T-Cross 2022", "valor_veiculo": 73224.0, }, "expires_at": utc_now() + timedelta(minutes=15), } }, "pending_cancel_order_drafts": { 1: { "payload": {"numero_pedido": "PED-20260312110556-BBA37F"}, "expires_at": utc_now() + timedelta(minutes=15), } }, }, contexts={ 1: { "active_domain": "sales", "active_task": "order_cancel", "generic_memory": {"cpf": "12345678909"}, "shared_memory": {"cpf": "12345678909"}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } }, ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_try_collect_and_create_order(**kwargs): raise AssertionError("nao deveria priorizar order_create enquanto order_cancel aguarda motivo") async def fake_try_collect_and_cancel_order(**kwargs): self.assertEqual(kwargs["message"], "desisti da compra") self.assertEqual(kwargs["user_id"], 1) return "Pedido PED-20260312110556-BBA37F atualizado.\nStatus: Cancelado" service._try_collect_and_create_order = fake_try_collect_and_create_order service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order async def finish(response: str, queue_notice: str | None = None): return response response = await service._try_handle_active_sales_follow_up( message="desisti da compra", user_id=1, finish=finish, ) self.assertIn("Pedido PED-20260312110556-BBA37F atualizado.", response) self.assertIn("Status: Cancelado", response) async def test_active_sales_follow_up_allows_new_budget_search_to_reset_open_order_draft(self): state = FakeState( entries={ "pending_order_drafts": { 1: { "payload": { "cpf": "12345678909", "vehicle_id": 15, "modelo_veiculo": "Volkswagen T-Cross 2022", "valor_veiculo": 73224.0, }, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "sales", "generic_memory": {"cpf": "12345678909", "orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "shared_memory": {"cpf": "12345678909", "orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [ {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True}, ], "selected_vehicle": {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True}, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_try_collect_and_create_order(**kwargs): raise AssertionError("nao deveria consumir nova busca de estoque no atalho de venda ativa") service._try_collect_and_create_order = fake_try_collect_and_create_order async def finish(response: str, queue_notice: str | None = None): return response response = await service._try_handle_active_sales_follow_up( message="agora quero comprar um carro de ate 60 mil", user_id=1, finish=finish, ) self.assertIsNone(response) self.assertIsNone(state.get_entry("pending_order_drafts", 1)) self.assertIsNone(state.get_user_context(1)["selected_vehicle"]) async def test_orchestration_control_ignores_cancel_flow_tool_for_fresh_sales_request_without_open_flow(self): state = FakeState( contexts={ 1: { "active_domain": "sales", "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) class DummyRegistry: def get_tools(self): return [] class DummyExecutor: async def execute(self, tool_name, arguments, user_id=None): raise AssertionError("nao deveria executar cancelar_fluxo_atual para nova busca operacional") service.registry = DummyRegistry() service.tool_executor = DummyExecutor() service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) service._has_open_flow = lambda user_id, domain: service.policy.has_open_flow(user_id=user_id, domain=domain) service._is_low_value_response = lambda text: False async def fake_call_llm_with_trace(**kwargs): return { "response": "", "tool_call": { "name": "cancelar_fluxo_atual", "arguments": {"motivo": "cliente mudou de ideia e iniciou nova busca"}, }, } service._call_llm_with_trace = fake_call_llm_with_trace async def finish(response: str, queue_notice: str | None = None): return response response = await service._try_execute_orchestration_control_tool( message="agora eu quero comprar um carro de ate 70 mil", user_id=1, turn_decision={}, extracted_entities={}, queue_notice=None, finish=finish, ) self.assertIsNone(response) def test_should_prioritize_order_flow_when_cancel_draft_is_open(self): state = FakeState( entries={ "pending_cancel_order_drafts": { 1: { "payload": {"numero_pedido": "PED-202603101204814-6ED33A"}, "expires_at": utc_now() + timedelta(minutes=15), } } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() prioritized = service._should_prioritize_order_flow( turn_decision={"intent": "general", "domain": "general", "action": "answer_user"}, extracted_entities={ "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, }, user_id=1, ) self.assertTrue(prioritized) def test_should_prioritize_order_flow_when_message_selects_vehicle_from_last_stock_results(self): state = FakeState( contexts={ 1: { "active_domain": "sales", "generic_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "shared_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]}, "last_stock_results": [ {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0}, {"id": 11, "modelo": "Toyota Corolla 2024", "categoria": "suv", "preco": 76087.0}, ], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) prioritized = service._should_prioritize_order_flow( turn_decision={"intent": "inventory_search", "domain": "sales", "action": "call_tool"}, extracted_entities={ "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, }, user_id=1, message="quero a opcao 1", ) self.assertTrue(prioritized) def test_should_prioritize_order_flow_for_explicit_order_request_without_entities(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) prioritized = service._should_prioritize_order_flow( turn_decision={"intent": "order_create", "domain": "sales", "action": "answer_user"}, extracted_entities={ "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, }, user_id=1, message="Quero fazer um pedido", ) self.assertTrue(prioritized) def test_should_prioritize_order_flow_for_explicit_order_request_even_when_model_returns_general(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) prioritized = service._should_prioritize_order_flow( turn_decision={"intent": "general", "domain": "general", "action": "answer_user"}, extracted_entities={ "generic_memory": {}, "review_fields": {}, "review_management_fields": {}, "order_fields": {}, "cancel_order_fields": {}, "intents": {}, }, user_id=1, message="Quero fazer um pedido", ) self.assertTrue(prioritized) def test_normalize_tool_name_maps_trade_in_alias_and_arguments(self): normalizer = EntityNormalizer() tool_name = normalizer.normalize_tool_name("avaliar_troca_veiculo") arguments = normalizer.normalize_tool_arguments( tool_name, {"modelo": "Onix", "ano": 2020, "quilometragem": 45000}, ) self.assertEqual(tool_name, "avaliar_veiculo_troca") self.assertEqual(arguments, {"modelo": "Onix", "ano": 2020, "km": 45000}) async def test_pending_order_selection_prefers_turn_decision_domain(self): state = FakeState( contexts={ 9: { "pending_order_selection": { "orders": [ {"domain": "review", "message": "agendar revisao", "memory_seed": {}}, {"domain": "sales", "message": "fazer pedido", "memory_seed": {}}, ], "expires_at": utc_now() + timedelta(minutes=15), }, "order_queue": [], "active_domain": "general", "generic_memory": {}, } } ) policy = ConversationPolicy(service=FakePolicyService(state)) response = await policy.try_resolve_pending_order_selection( message="quero comprar", user_id=9, turn_decision={"domain": "sales", "intent": "order_create", "action": "collect_order_create"}, ) self.assertIn("Vou comecar por: Venda: fazer pedido", response) async def test_pending_order_selection_prefers_turn_decision_selection_index(self): state = FakeState( contexts={ 9: { "pending_order_selection": { "orders": [ {"domain": "review", "message": "agendar revisao", "memory_seed": {}}, {"domain": "sales", "message": "fazer pedido", "memory_seed": {}}, ], "expires_at": utc_now() + timedelta(minutes=15), }, "order_queue": [], "active_domain": "general", "generic_memory": {}, } } ) policy = ConversationPolicy(service=FakePolicyService(state)) response = await policy.try_resolve_pending_order_selection( message="esse", user_id=9, turn_decision={"domain": "general", "intent": "general", "action": "answer_user", "selection_index": 1}, ) self.assertIn("Vou comecar por: Venda: fazer pedido", response) async def test_pending_order_selection_promotes_new_operational_request_before_previous_options(self): state = FakeState( contexts={ 9: { "pending_order_selection": { "orders": [ {"domain": "sales", "message": "compra", "seed_message": "quero comprar um veiculo", "memory_seed": {}}, {"domain": "review", "message": "revisao", "seed_message": "quero agendar revisao", "memory_seed": {}}, {"domain": "rental", "message": "aluguel", "seed_message": "quero alugar um carro", "memory_seed": {}}, ], "expires_at": utc_now() + timedelta(minutes=15), }, "order_queue": [], "active_domain": "general", "generic_memory": {}, } } ) service = FakePolicyService(state) policy = ConversationPolicy(service=service) response = await policy.try_resolve_pending_order_selection( message="quais pedidos eu tenho?", user_id=9, turn_decision={"domain": "sales", "intent": "order_list", "action": "call_tool", "tool_name": "listar_pedidos"}, ) self.assertIsNone(response) context = state.get_user_context(9) self.assertIsNone(context["pending_order_selection"]) self.assertEqual([item["domain"] for item in context["order_queue"]], ["sales", "review", "rental"]) self.assertEqual(context["order_queue"][0]["message"], "quero comprar um veiculo") async def test_pending_order_selection_skips_duplicate_base_task_when_new_request_is_more_specific(self): state = FakeState( contexts={ 9: { "pending_order_selection": { "orders": [ {"domain": "sales", "message": "compra", "seed_message": "quero comprar um veiculo", "memory_seed": {}}, {"domain": "review", "message": "revisao", "seed_message": "quero agendar revisao", "memory_seed": {}}, {"domain": "rental", "message": "aluguel", "seed_message": "quero alugar um carro", "memory_seed": {}}, ], "expires_at": utc_now() + timedelta(minutes=15), }, "order_queue": [], "active_domain": "general", "generic_memory": {}, } } ) service = FakePolicyService(state) policy = ConversationPolicy(service=service) response = await policy.try_resolve_pending_order_selection( message="quero comprar um suv ate 95 mil", user_id=9, turn_decision={"domain": "sales", "intent": "order_create", "action": "collect_order_create"}, ) self.assertIn("Perfeito. Vou comecar por: Venda: compra", response) self.assertIn("handled:quero comprar um suv ate 95 mil", response) context = state.get_user_context(9) self.assertIsNone(context["pending_order_selection"]) self.assertEqual(context["active_domain"], "sales") self.assertEqual([item["domain"] for item in context["order_queue"]], ["review", "rental"]) async def test_try_continue_queue_prefers_turn_decision_action(self): state = FakeState( contexts={ 9: { "pending_switch": { "target_domain": "sales", "queued_message": "fazer pedido", "memory_seed": {"cpf": "12345678909"}, "expires_at": utc_now() + timedelta(minutes=15), }, "active_domain": "general", "generic_memory": {}, "pending_order_selection": None, } } ) service = FakePolicyService(state) policy = ConversationPolicy(service=service) policy.apply_domain_switch = lambda user_id, target_domain: service._get_user_context(user_id).update( {"active_domain": target_domain, "pending_switch": None} ) response = await policy.try_continue_queued_order( message="ok", user_id=9, turn_decision={"action": "continue_queue", "intent": "queue_continue", "domain": "sales"}, ) self.assertIn("Agora, sobre a compra do veiculo:", response) def test_handle_context_switch_prefers_turn_decision_domain_confirmation(self): state = FakeState( contexts={ 9: { "pending_switch": { "target_domain": "review", "expires_at": utc_now() + timedelta(minutes=15), }, "active_domain": "sales", "generic_memory": {}, "pending_order_selection": None, } } ) service = FakePolicyService(state) policy = ConversationPolicy(service=service) policy.apply_domain_switch = lambda user_id, target_domain: service._get_user_context(user_id).update( {"active_domain": target_domain, "pending_switch": None} ) response = policy.handle_context_switch( message="quero revisar", user_id=9, target_domain_hint="review", turn_decision={"domain": "review", "intent": "review_schedule", "action": "collect_review_schedule"}, ) self.assertEqual( response, "Certo, contexto anterior encerrado. Vamos seguir com agendamento de revisao.\n" "Pode me informar a placa ou, se preferir, ja mandar placa, data/hora, modelo, ano, km e se ja fez revisao.", ) def test_prepare_message_for_single_order_defers_explicit_domain_switch_with_open_flow(self): state = FakeState( entries={ "pending_review_drafts": { 9: { "payload": {"placa": "ABC1234"}, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 9: { "active_domain": "review", "generic_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, } }, ) service = FakePolicyService(state) policy = ConversationPolicy(service=service) routed_message, queue_notice, early_response = policy.prepare_message_for_single_order( message="quero comprar um carro de ate 62 mil", user_id=9, routing_plan={"orders": [{"domain": "sales", "message": "quero comprar um carro de ate 62 mil"}]}, ) self.assertEqual(routed_message, "quero comprar um carro de ate 62 mil") self.assertIsNone(queue_notice) self.assertIsNone(early_response) self.assertEqual(service._get_user_context(9).get("order_queue"), []) def test_prepare_message_for_single_order_requests_clarification_for_three_actionable_domains(self): state = FakeState( contexts={ 9: { "active_domain": "general", "generic_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, } }, ) service = FakePolicyService(state) policy = ConversationPolicy(service=service) routed_message, queue_notice, early_response = policy.prepare_message_for_single_order( message="oi, pode me ajudar com compra, revisao e aluguel?", user_id=9, routing_plan={ "orders": [ {"domain": "sales", "message": "compra"}, {"domain": "review", "message": "revisao"}, ] }, ) self.assertEqual(routed_message, "oi, pode me ajudar com compra, revisao e aluguel?") self.assertIsNone(queue_notice) self.assertIn("Identifiquei 3 acoes", early_response) self.assertIn("3. Locacao: aluguel", early_response) pending = state.get_user_context(9)["pending_order_selection"] self.assertEqual(len(pending["orders"]), 3) def test_prepare_message_for_single_order_counts_only_orders_effectively_queued(self): state = FakeState( entries={ "pending_review_drafts": { 9: { "payload": {"placa": "ABC1234"}, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 9: { "active_domain": "review", "generic_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, } }, ) service = FakePolicyService(state) policy = ConversationPolicy(service=service) routed_message, queue_notice, early_response = policy.prepare_message_for_single_order( message="quero continuar a revisao e tambem ver aluguel", user_id=9, routing_plan={ "orders": [ {"domain": "review", "message": "quero continuar a revisao"}, {"domain": "general", "message": "oi"}, {"domain": "rental", "message": "quero ver aluguel"}, ] }, ) self.assertEqual(routed_message, "quero continuar a revisao e tambem ver aluguel") self.assertIn("Anotei mais 1 pedido", early_response) self.assertEqual(len(state.get_user_context(9)["order_queue"]), 1) self.assertEqual(state.get_user_context(9)["order_queue"][0]["domain"], "rental") self.assertEqual(state.get_user_context(9)["order_queue"][0]["message"], "quero ver aluguel") async def test_pending_order_selection_uses_canonical_seed_message_for_selected_domain(self): state = FakeState( contexts={ 9: { "active_domain": "general", "generic_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, } } ) service = FakePolicyService(state) policy = ConversationPolicy(service=service) policy.store_pending_order_selection( user_id=9, orders=[ {"domain": "sales", "message": "compra", "entities": service.normalizer.empty_extraction_payload()}, {"domain": "review", "message": "revisao", "entities": service.normalizer.empty_extraction_payload()}, {"domain": "rental", "message": "aluguel", "entities": service.normalizer.empty_extraction_payload()}, ], ) response = await policy.try_resolve_pending_order_selection(message="1", user_id=9) self.assertIn("Perfeito. Vou comecar por: Venda: compra", response) self.assertIn("handled:quero comprar um veiculo", response) context = state.get_user_context(9) self.assertEqual(context["active_domain"], "sales") self.assertEqual([item["domain"] for item in context["order_queue"]], ["review", "rental"]) async def test_tool_continuar_proximo_pedido_reports_empty_queue(self): state = FakeState( contexts={ 1: { "active_domain": "sales", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) response = await service._tool_continuar_proximo_pedido(user_id=1) self.assertEqual(response, "Nao ha pedidos pendentes na fila para continuar.") async def test_tool_cancelar_fluxo_atual_preserves_queue(self): state = FakeState( entries={ "pending_order_drafts": { 1: { "payload": {"vehicle_id": 7}, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "sales", "active_task": "order_create", "generic_memory": {"orcamento_max": 70000}, "shared_memory": {"orcamento_max": 70000}, "order_queue": [{"domain": "review", "message": "agendar revisao"}], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [{"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0}], "selected_vehicle": {"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0}, "last_rental_contract": { "contrato_numero": "LOC-20260319-33CD6567", "placa": "RAA1A02", }, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) result = await service._tool_cancelar_fluxo_atual(user_id=1) self.assertEqual(result["message"], "Fluxo atual de compra de veiculo cancelado.") self.assertIsNone(state.get_entry("pending_order_drafts", 1)) self.assertEqual(state.get_user_context(1)["order_queue"], [{"domain": "review", "message": "agendar revisao"}]) async def test_tool_descartar_pedidos_pendentes_preserves_active_flow(self): state = FakeState( entries={ "pending_order_drafts": { 1: { "payload": {"vehicle_id": 7}, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "sales", "active_task": "order_create", "generic_memory": {"orcamento_max": 70000}, "shared_memory": {"orcamento_max": 70000}, "order_queue": [{"domain": "review", "message": "agendar revisao"}], "pending_order_selection": { "orders": [ {"domain": "sales", "message": "comprar carro"}, {"domain": "review", "message": "remarcar revisao"}, ], "expires_at": utc_now() + timedelta(minutes=15), }, "pending_switch": { "target_domain": "review", "queued_message": "agendar revisao", "expires_at": utc_now() + timedelta(minutes=15), }, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) result = await service._tool_descartar_pedidos_pendentes(user_id=1) self.assertEqual(result["message"], "Descartei 4 pedidos pendentes da fila.") self.assertIsNotNone(state.get_entry("pending_order_drafts", 1)) self.assertEqual(state.get_user_context(1)["order_queue"], []) self.assertIsNone(state.get_user_context(1)["pending_order_selection"]) self.assertIsNone(state.get_user_context(1)["pending_switch"]) async def test_handle_message_prioritizes_trade_in_evaluation_over_freeform_answer(self): state = FakeState( contexts={ 1: { "active_domain": "general", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service.policy = ConversationPolicy(service=service) service.tool_executor = FakeToolExecutor( result={ "modelo": "Onix", "ano": 2020, "km": 45000, "valor_estimado_troca": 65432.0, } ) service._empty_extraction_payload = service.normalizer.empty_extraction_payload service._log_turn_event = lambda *args, **kwargs: None service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response service._fallback_format_tool_result = lambda tool_name, tool_result: ( "Estimativa de troca concluida.\n" f"Veiculo: {tool_result['modelo']} {tool_result['ano']}\n" f"Quilometragem: {tool_result['km']} km\n" f"Valor estimado: R$ {tool_result['valor_estimado_troca']:.2f}" ) service._http_exception_detail = lambda exc: str(exc) service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None): return base_response service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order service._upsert_user_context = lambda user_id: None async def fake_try_handle_pending_stock_selection_follow_up(**kwargs): return None service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up async def fake_try_handle_active_sales_follow_up(**kwargs): return None service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up async def fake_try_handle_active_review_follow_up(**kwargs): return None service._try_handle_active_review_follow_up = fake_try_handle_active_review_follow_up async def fake_try_handle_immediate_context_reset(**kwargs): return None service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset async def fake_try_resolve_pending_order_selection(**kwargs): return None service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection async def fake_try_continue_queued_order(**kwargs): return None service._try_continue_queued_order = fake_try_continue_queued_order async def fake_extract_turn_decision(message: str, user_id: int | None): return { "intent": "general", "domain": "general", "action": "answer_user", "entities": service.normalizer.empty_extraction_payload(), "missing_fields": [], "selection_index": None, "tool_name": None, "tool_arguments": {}, "response_to_user": "Legal! Para fazer a avaliacao, preciso da versao e da placa.", } service._extract_turn_decision_with_llm = fake_extract_turn_decision async def fake_extract_message_plan(message: str, user_id: int | None): return { "orders": [ { "domain": "sales", "message": message, "entities": service.normalizer.empty_extraction_payload(), } ] } service._extract_message_plan_with_llm = fake_extract_message_plan service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None) service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload() async def fake_extract_entities(message: str, user_id: int | None): return service.normalizer.empty_extraction_payload() service._extract_entities_with_llm = fake_extract_entities async def fake_extract_missing_sales_search_context_with_llm(**kwargs): return {} service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm service._domain_from_intents = lambda intents: "general" service._handle_context_switch = lambda **kwargs: None service._update_active_domain = lambda **kwargs: None async def fake_try_execute_orchestration_control_tool(**kwargs): return None service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool async def fake_try_handle_review_management(**kwargs): raise AssertionError("nao deveria entrar em gerenciamento de revisao para avaliacao de troca") service._try_handle_review_management = fake_try_handle_review_management async def fake_try_confirm_pending_review(**kwargs): raise AssertionError("nao deveria entrar em confirmacao de revisao para avaliacao de troca") service._try_confirm_pending_review = fake_try_confirm_pending_review async def fake_try_collect_and_schedule_review(**kwargs): raise AssertionError("nao deveria entrar em agendamento de revisao para avaliacao de troca") service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def fake_try_collect_and_cancel_order(**kwargs): raise AssertionError("nao deveria entrar em cancelamento de pedido para avaliacao de troca") service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order async def fake_try_handle_order_listing(**kwargs): raise AssertionError("nao deveria entrar em listagem de pedidos para avaliacao de troca") service._try_handle_order_listing = fake_try_handle_order_listing async def fake_try_collect_and_create_order(**kwargs): raise AssertionError("nao deveria entrar em compra para avaliacao de troca") service._try_collect_and_create_order = fake_try_collect_and_create_order response = await service.handle_message( "Quero avaliar meu carro para troca: Onix 2020, 45000 km", user_id=1, ) self.assertIn("Estimativa de troca concluida", response) self.assertEqual( service.tool_executor.calls, [("avaliar_veiculo_troca", {"modelo": "Onix", "ano": 2020, "km": 45000}, 1)], ) async def test_active_review_follow_up_ignores_trade_in_request(self): state = FakeState( entries={ "pending_review_drafts": { 1: { "payload": {"placa": "ABC1234"}, "expires_at": utc_now() + timedelta(minutes=15), } } }, contexts={ 1: { "active_domain": "review", "generic_memory": {}, "shared_memory": {}, "order_queue": [], "pending_order_selection": None, "pending_switch": None, "last_stock_results": [], "selected_vehicle": None, } } ) service = OrquestradorService.__new__(OrquestradorService) service.state = state service.normalizer = EntityNormalizer() service._get_user_context = lambda user_id: state.get_user_context(user_id) service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context) async def fake_try_collect_and_schedule_review(**kwargs): raise AssertionError("nao deveria consumir avaliacao de troca como follow-up de revisao") service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review async def finish(response: str, queue_notice: str | None = None): return response response = await service._try_handle_active_review_follow_up( message="Quero avaliar meu carro para troca: Onix 2020, 45000 km", user_id=1, finish=finish, ) self.assertIsNone(response) class OrquestradorEmailCaptureTests(unittest.IsolatedAsyncioTestCase): def _build_service(self, state=None): service = OrquestradorService.__new__(OrquestradorService) service.state = state or FakeState() service.normalizer = EntityNormalizer() service._turn_trace = {"request_id": "req-1"} return service def test_stage_email_capture_request_and_prompt_for_current_turn(self): service = self._build_service() service._get_saved_user_email = lambda user_id: None service._stage_email_capture_request( tool_name="realizar_pedido", tool_result={"numero_pedido": "PED-1"}, user_id=7, ) pending = service.state.get_entry("pending_email_capture_requests", 7) self.assertIsNotNone(pending) self.assertEqual(pending["event_type"], ORDER_CREATED_EVENT) self.assertEqual(pending["payload"]["numero_pedido"], "PED-1") self.assertEqual(pending["payload"]["user_id"], 7) response = service._append_email_capture_prompt_if_needed( response="Pedido criado com sucesso.", user_id=7, ) self.assertIn("Se quiser, posso te enviar esse resumo por e-mail.", response) async def test_pending_email_capture_decline_clears_request(self): state = FakeState( entries={ "pending_email_capture_requests": { 7: { "request_id": "req-1", "event_type": ORDER_CREATED_EVENT, "payload": {"numero_pedido": "PED-1", "user_id": 7}, "expires_at": utc_now() + timedelta(minutes=15), } } } ) service = self._build_service(state=state) response = await service._try_handle_pending_email_capture_message( message="prefiro nao informar", user_id=7, ) self.assertEqual(response, "Tudo bem. Nao vou enviar este resumo por e-mail.") self.assertIsNone(state.get_entry("pending_email_capture_requests", 7)) def test_ensure_user_email_routes_syncs_global_routes_only_once(self): service = self._build_service() service._get_user_record = lambda user_id: SimpleNamespace( id=user_id, email="cliente@example.com", name="Cliente Teste", ) with patch( "app.services.orchestration.orquestrador_service.sync_user_email_integration_routes" ) as sync_routes_mock: service._ensure_user_email_routes(user_id=7) service._ensure_user_email_routes(user_id=7) sync_routes_mock.assert_called_once_with( user_id=7, recipient_email="cliente@example.com", recipient_name="Cliente Teste", ) async def test_pending_email_capture_success_saves_email_and_reemits_event(self): state = FakeState( entries={ "pending_email_capture_requests": { 7: { "request_id": "req-1", "event_type": ORDER_CREATED_EVENT, "payload": {"numero_pedido": "PED-1", "user_id": 7}, "expires_at": utc_now() + timedelta(minutes=15), } } } ) service = self._build_service(state=state) saved = {} ensured_routes = [] def fake_save_user_email(user_id: int | None, email: str | None): saved["user_id"] = user_id saved["email"] = email return SimpleNamespace(id=user_id, email=email, name="Cliente Teste") service._save_user_email = fake_save_user_email service._ensure_user_email_routes = lambda user_id: ensured_routes.append(user_id) with patch( "app.services.orchestration.orquestrador_service.emit_business_event", new=AsyncMock(return_value=[{"status": "sent", "provider_message_id": "brevo-1"}]), ) as emit_business_event_mock: response = await service._try_handle_pending_email_capture_message( message="cliente@example.com", user_id=7, ) self.assertEqual(saved, {"user_id": 7, "email": "cliente@example.com"}) self.assertEqual(ensured_routes, [7]) emit_business_event_mock.assert_awaited_once_with( event_type=ORDER_CREATED_EVENT, payload={"numero_pedido": "PED-1", "user_id": 7}, ) self.assertIn("enviei este resumo por la", response) self.assertIsNone(state.get_entry("pending_email_capture_requests", 7)) if __name__ == "__main__": unittest.main()