from __future__ import annotations import pytest from app.extensions.tim_guardrails import TimProactiveOfferRail class _FailIfCalledLLM: async def ainvoke(self, *args, **kwargs): raise AssertionError('TIM_AOFERTA LLM must not run for transaction continuation') class _BlockingLLM: def __init__(self): self.calls = 0 async def ainvoke(self, *args, **kwargs): self.calls += 1 return '{"allowed": false, "reason": "oferta proativa"}' @pytest.mark.asyncio @pytest.mark.parametrize('status', ['COLLECTING_PARAMETERS', 'AWAITING_CONFIRMATION']) async def test_tim_aoferta_bypasses_native_transaction_continuation_states(status): decision = await TimProactiveOfferRail().evaluate( 'Você confirma o cancelamento do serviço TIM Fashion?', { 'transaction_status': status, 'guardrail_llm': _FailIfCalledLLM(), }, ) assert decision.allowed is True assert decision.code == 'TIM_AOFERTA' assert decision.reason == f'continuidade_transacional:{status}' assert decision.metadata['mechanism'] == 'deterministic_transaction_bypass' assert decision.metadata['transaction_status'] == status @pytest.mark.asyncio async def test_tim_aoferta_detects_awaiting_confirmation_from_mcp_results(): decision = await TimProactiveOfferRail().evaluate( 'Você confirma o cancelamento do serviço TIM Fashion?', { 'mcp_results': [ { 'tool_name': 'cancelar_vas_avulso', 'awaiting_confirmation': True, 'transaction_status': 'AWAITING_CONFIRMATION', } ], 'guardrail_llm': _FailIfCalledLLM(), }, ) assert decision.allowed is True assert decision.metadata['transaction_status'] == 'AWAITING_CONFIRMATION' @pytest.mark.asyncio async def test_tim_aoferta_detects_parameter_collection_from_tool_result(): decision = await TimProactiveOfferRail().evaluate( 'Para prosseguir, informe valor.', { 'tool_result': [ { 'tool_name': 'contestar_cobranca', 'transaction_status': 'COLLECTING_PARAMETERS', } ], 'guardrail_llm': _FailIfCalledLLM(), }, ) assert decision.allowed is True assert decision.metadata['transaction_status'] == 'COLLECTING_PARAMETERS' @pytest.mark.asyncio async def test_tim_aoferta_still_uses_llm_outside_transaction_continuation(): llm = _BlockingLLM() decision = await TimProactiveOfferRail().evaluate( 'Caso queira, posso cancelar outro serviço.', { 'transaction_status': 'COMPLETED', 'guardrail_llm': llm, }, ) assert llm.calls == 1 assert decision.allowed is False assert decision.reason == 'oferta proativa' @pytest.mark.asyncio async def test_tim_aoferta_allows_structurally_authorized_human_handoff_without_calling_llm(): decision = await TimProactiveOfferRail().evaluate( 'Vou encaminhar seu atendimento para uma pessoa.', { 'current_route': 'human_handoff', 'current_intent': 'human_handoff', 'session_control': 'HUMAN_HANDOFF', 'human_handoff_requested': True, 'guardrail_llm': _FailIfCalledLLM(), }, ) assert decision.allowed is True assert decision.reason == 'handoff_humano_autorizado' assert decision.metadata['mechanism'] == 'deterministic_handoff_bypass' @pytest.mark.asyncio async def test_tim_aoferta_does_not_bypass_transfer_phrase_without_structural_handoff(): llm = _BlockingLLM() decision = await TimProactiveOfferRail().evaluate( 'Vou encaminhar seu atendimento para uma pessoa.', { 'current_route': 'faturas_agent', 'current_intent': 'contas_invoice_query', 'guardrail_llm': llm, }, ) assert llm.calls == 1 assert decision.allowed is False @pytest.mark.asyncio async def test_tim_aoferta_allows_terminal_handoff_from_resumed_workflow_without_router_handoff(): decision = await TimProactiveOfferRail().evaluate( 'Para continuar com a sua solicitação, aguarde um instante.', { 'current_route': 'faturas_agent', 'current_intent': 'contas_invoice_explanation', 'mcp_results': [{ 'tool_name': 'retomar_workflow', 'result': { 'status': 'COMPLETED', 'output': { 'mensagem': 'Para continuar com a sua solicitação, aguarde um instante.', 'session_control': 'HUMAN_HANDOFF', 'human_handoff_requested': True, 'handoff': True, 'session_ended': True, 'terminal_status': 'human_handoff', }, }, }], 'guardrail_llm': _FailIfCalledLLM(), }, ) assert decision.allowed is True assert decision.reason == 'handoff_humano_autorizado' assert decision.metadata['mechanism'] == 'deterministic_handoff_bypass' @pytest.mark.asyncio async def test_tim_aoferta_does_not_trust_non_terminal_workflow_handoff_flag(): llm = _BlockingLLM() decision = await TimProactiveOfferRail().evaluate( 'Vou encaminhar seu atendimento para uma pessoa.', { 'current_route': 'faturas_agent', 'current_intent': 'contas_invoice_explanation', 'mcp_results': [{ 'result': { 'output': { 'handoff': True, }, }, }], 'guardrail_llm': llm, }, ) assert llm.calls == 1 assert decision.allowed is False class _CaptureAllowLLM: def __init__(self): self.prompt = '' async def ainvoke(self, messages, **kwargs): self.prompt = messages[-1]['content'] return '{"allowed": true, "reason": ""}' @pytest.mark.asyncio async def test_tim_aoferta_terminal_fallback_receives_compact_authoritative_transaction_context(): llm = _CaptureAllowLLM() huge = {'noise': 'x' * 50000} decision = await TimProactiveOfferRail().evaluate( 'Não consegui cancelar o Paramount+ por aqui. Use o canal oficial para concluir esse mesmo cancelamento.', { 'transaction_status': 'COMPLETED', 'current_user_message': 'isso mesmo, pode cancelar', 'conversation_history': [ {'role': 'user', 'content': 'quero cancelar Tamboro e Paramount+'}, {'role': 'assistant', 'content': 'Você confirma o cancelamento de Tamboro e Paramount+?'}, {'role': 'user', 'content': 'isso mesmo, pode cancelar'}, ], 'transaction_pre_validation': { 'requested_arguments': {'subject': 'Tamboro e Paramount+'}, 'resolved_arguments': {'subject': 'Tamboro Mensal, Paramount+', 'items': [{'name': 'Tamboro Mensal'}, {'name': 'Paramount+'}]}, 'effective_tool_name': 'cancelar_vas_avulso', }, 'mcp_results': [ huge, { 'tool_name': 'cancelar_vas_avulso', 'ok': True, 'result': { 'status': 'COMPLETED', 'output': { 'results': [ {'subject': 'Tamboro Mensal', 'success': True}, {'subject': 'Paramount+', 'success': False, 'reason': 'service_not_found'}, ] }, }, }, ], 'guardrail_llm': llm, }, ) assert decision.allowed is True assert decision.metadata['mechanism'] == 'llm_semantic_compact_transaction_context' assert 'Paramount+' in llm.prompt assert 'service_not_found' in llm.prompt assert 'isso mesmo, pode cancelar' in llm.prompt assert len(llm.prompt) < 30000