from __future__ import annotations import pytest from app.extensions.tim_guardrails import TimOutOfScopeRail class _FailIfCalledLLM: async def ainvoke(self, *args, **kwargs): raise AssertionError('TIM_OOS LLM must not run for structurally authorized human handoff') class _BlockingLLM: def __init__(self): self.calls = 0 async def ainvoke(self, *args, **kwargs): self.calls += 1 return '{"allowed": false, "reason": "mensagem fora do escopo de contas/faturas TIM"}' @pytest.mark.asyncio async def test_tim_oos_allows_structurally_authorized_human_handoff_without_calling_llm(): decision = await TimOutOfScopeRail().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.code == 'TIM_OOS' assert decision.reason == 'handoff_humano_autorizado' assert decision.metadata['mechanism'] == 'deterministic_handoff_bypass' @pytest.mark.asyncio async def test_tim_oos_allows_route_decision_structural_handoff_without_calling_llm(): decision = await TimOutOfScopeRail().evaluate( 'Vou encaminhar seu atendimento para uma pessoa.', { 'route_decision': { 'route': 'human_handoff', 'intent': 'human_handoff', 'handoff': True, 'metadata': {'session_control': 'HUMAN_HANDOFF'}, }, 'guardrail_llm': _FailIfCalledLLM(), }, ) assert decision.allowed is True assert decision.metadata['mechanism'] == 'deterministic_handoff_bypass' @pytest.mark.asyncio async def test_tim_oos_does_not_bypass_transfer_phrase_without_structural_handoff(): llm = _BlockingLLM() decision = await TimOutOfScopeRail().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 assert decision.reason == 'mensagem fora do escopo de contas/faturas TIM' @pytest.mark.asyncio async def test_tim_oos_requires_route_and_control_evidence_not_only_handoff_boolean(): llm = _BlockingLLM() decision = await TimOutOfScopeRail().evaluate( 'Vou encaminhar seu atendimento para uma pessoa.', { 'handoff': True, '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_oos_allows_terminal_handoff_from_resumed_workflow_without_router_handoff(): decision = await TimOutOfScopeRail().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': { '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_oos_does_not_trust_non_terminal_workflow_handoff_flag(): llm = _BlockingLLM() decision = await TimOutOfScopeRail().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