from __future__ import annotations from types import SimpleNamespace from agent_framework.channels.interruption import evaluate_interruption from agent_framework.guardrails.calibrated.contracts import GuardRailContext from agent_framework.guardrails.calibrated.rails.anatel import AnatelRail from app.domain.contas.vas_variation import VariedCharge, varied_current_charges from app.domain.contas.workflow_actions import build_contas_workflow_actions PAST = "2026-03-10T00:00:00.000Z" CURRENT = "2026-04-07T00:00:00.000Z" def _group(*items: dict, type_: str = "servicos_contratados_de_parceiros") -> dict: return {"type": type_, "desc": "Serviços de valor adicionado", "items": list(items)} def test_vas_variation_preserva_multiset_e_direcao_atual(): payload = {"invoiceVariation": [ _group( {"desc": "Qualifica Mensal", "value": "19.99", "invoice": PAST}, {"desc": "Qualifica Mensal", "value": "19.99", "invoice": CURRENT}, {"desc": "Qualifica Mensal", "value": "19.99", "invoice": CURRENT}, ) ]} result = varied_current_charges(payload) assert result == (VariedCharge(desc="Qualifica Mensal", value=result[0].value),) assert str(result[0].value) == "19.99" def test_vas_variation_streaming_nao_entra_no_recorte_avulso_padrao(): payload = {"invoiceVariation": [ _group( {"desc": "Netflix Padrão", "value": "20.90", "invoice": CURRENT}, type_="streaming", ) ]} assert varied_current_charges(payload) == () def test_post_finalize_replay_e_short_circuit_framework(): decision = evaluate_interruption( payload={}, message_text="alô, ainda está aí?", session_metadata={ "conversation_closed": True, "terminal_status": "nao_resolvido", "last_assistant_text": "Aguarde um instante na linha. Protocolo: 2026492272073", "last_assistant_is_interruptible": True, }, ) assert decision.action == "replay" assert decision.reason == "post_finalize" assert "2026492272073" in decision.replay_text assert decision.is_interruptible is False def test_anatel_rail_e_framework_native_sem_dependencia_legado(): rail = AnatelRail() blocked = rail.evaluate( GuardRailContext( session_id="s1", user_text="O ajuste foi concluído.", agent_metadata={ "tipo_fluxo": "ajuste", "requer_protocolo": True, "expected_protocols": ["2026492272073"], }, ) ) assert blocked.allowed is False assert blocked.sanitized_text assert "protocolo" in blocked.sanitized_text.lower() approved = rail.evaluate( GuardRailContext( session_id="s1", user_text="Ajuste concluído. Protocolo: 2026492272073.", agent_metadata={"tipo_fluxo": "ajuste"}, ) ) assert approved.allowed is True class _FakeClient: def __init__(self): self.contestar_calls = 0 def billing_analysis(self, msisdn: str): return { "Servicos de Valor Adicionado": [ {"desc": "Pacote Musica", "valor": "10,00"} ] } def contestar(self, payload): self.contestar_calls += 1 return {"ok": True} class _FakeService: def __init__(self): self.client = _FakeClient() def test_cval_bloqueia_contestacao_antes_do_side_effect(): service = _FakeService() reg = build_contas_workflow_actions(service) # type: ignore[arg-type] action = reg.get("abrir_contestacao_cliente") result = action( { "msisdn": "11999999999", "items": [ { "itemName": "Pacote nao cobrado", "claimedAmount": "10", "validatedAmount": "10", } ], }, {"input": {}}, ) assert result["success"] is False assert result["blocked"] is True assert result["guardrail_code"] == "CVAL" assert service.client.contestar_calls == 0 from app.domain.contas.item_matcher import SimilarityItemMatcher def test_similarity_matcher_recupera_erros_de_transcricao_comuns(): matcher = SimilarityItemMatcher() candidates = [ "Tamboro Mensal", "Netflix Padrão", "Truecaller Semanal", "MasterChef Mensal", "Paramount+ Mensal", ] assert matcher.match("mestershef", candidates)[0] == "MasterChef Mensal" assert matcher.match("trucaller", candidates)[0] == "Truecaller Semanal" assert matcher.match("paramaunt", candidates)[0] == "Paramount+ Mensal" def test_tool_clarification_aceita_opcao_ordinal(): from agent_framework.runtime.agent_runtime import AgentRuntimeMixin options = [ {"label": "Tamboro Mensal — linha final 1111", "value": "Tamboro Mensal"}, {"label": "Tamboro Mensal — linha final 2222", "value": "Tamboro Mensal"}, ] selected = AgentRuntimeMixin._choose_tool_clarification_option("2", options) assert selected == options[1] selected_by_name = AgentRuntimeMixin._choose_tool_clarification_option( "Tamboro Mensal — linha final 1111", options ) assert selected_by_name == options[0] def test_output_guardrail_context_extrai_protocolos_das_tools(): from app.workflows.agent_graph import AgentWorkflow ctx = AgentWorkflow._output_guardrail_context( { "context": {}, "mcp_results": [ { "ok": True, "result": { "result": { "protocolo_id": "2026492272073", "nested": {"protocolNumber": "2026492272074"}, } }, } ], } ) assert ctx["requer_protocolo"] is True assert ctx["tipo_fluxo"] == "ajuste" assert ctx["expected_protocols"] == ["2026492272073", "2026492272074"] assert ctx["tool_executed"] is True def test_fix_whole_utterance_transcription_preserva_fronteira_de_frase(): from agent_framework.channels.transcription import fix_whole_utterance_transcription assert fix_whole_utterance_transcription("Fim") == "Sim" assert fix_whole_utterance_transcription("Mim?") == "Sim" assert fix_whole_utterance_transcription("chegou ao fim") == "chegou ao fim" assert fix_whole_utterance_transcription("isso é pra mim") == "isso é pra mim" def test_post_finalize_replay_preserva_terminal_status_e_fallback_sem_historico(): decision = evaluate_interruption( payload={}, message_text="oi", session_metadata={ "conversation_closed": True, "terminal_status": "nao_resolvido", "last_assistant_text": "", "terminal_replay_text": "", }, terminal_fallback_text=( "Por aqui finalizamos o tratamento da sua solicitação. " "Aguarde um instante na linha." ), ) assert decision.action == "replay" assert decision.reason == "post_finalize" assert decision.terminal_status == "nao_resolvido" assert decision.replay_text.startswith("Por aqui finalizamos") assert decision.is_interruptible is False def test_post_finalize_replay_status_invalido_usa_fallback_seguro(): decision = evaluate_interruption( payload={}, message_text="oi", session_metadata={"conversation_closed": True}, terminal_fallback_text="Atendimento já encerrado.", ) assert decision.action == "replay" assert decision.terminal_status == "erro_falha_sistema" assert decision.replay_text == "Atendimento já encerrado." import pytest @pytest.mark.parametrize( ("entrada", "esperado"), [ ("Fim", "Sim"), ("fim", "Sim"), ("FIM", "Sim"), ("Fim.", "Sim"), (" fim ", "Sim"), ("fim!", "Sim"), ("Mim", "Sim"), ("mim", "Sim"), ("Mim?", "Sim"), ("chegou ao fim", "chegou ao fim"), ("isso é pra mim", "isso é pra mim"), ("fim de semana", "fim de semana"), ("Fim, pode encerrar", "Fim, pode encerrar"), ("mim mesmo", "mim mesmo"), ("Sim", "Sim"), ("não", "não"), ("", ""), (" ", " "), ("cancela o Tamboro", "cancela o Tamboro"), ], ) def test_transcription_fix_paridade_original(entrada: str, esperado: str): from agent_framework.channels.transcription import fix_whole_utterance_transcription assert fix_whole_utterance_transcription(entrada) == esperado class _ClassifierResponse: def __init__(self, content: str): self.content = content class _ClassifierLLM: def __init__(self, content: str = "0", *, fail: bool = False): self.content = content self.fail = fail self.calls = [] async def ainvoke(self, messages, **kwargs): self.calls.append((messages, kwargs)) if self.fail: raise RuntimeError("classifier unavailable") return _ClassifierResponse(self.content) def test_processing_interruption_interrompivel_pede_classificacao(): decision = evaluate_interruption( payload={"processing_interruption": {"heard_text": "cancelar também"}}, message_text="cancelar também", session_metadata={ "last_assistant_text": "Vou verificar sua fatura.", "last_assistant_is_interruptible": True, }, ) assert decision.action == "classify" assert decision.replay_text == "Vou verificar sua fatura." assert decision.heard_text == "cancelar também" @pytest.mark.asyncio async def test_processing_interruption_classifier_resultado_1_reprocessa(): from agent_framework.channels.interruption import classify_processing_interruption llm = _ClassifierLLM("1") result = await classify_processing_interruption( llm, original_agent="Vou orientar sobre o cancelamento.", original_client="Quero cancelar VOD e o", supplement_client="Aluguel de Filme 1.", ) assert result is True assert llm.calls _, kwargs = llm.calls[0] assert kwargs["profile_name"] == "processing_interruption_classifier" assert kwargs["component_name"] == "processing_interruption_classifier" @pytest.mark.asyncio async def test_processing_interruption_classifier_falha_faz_replay_seguro(): from agent_framework.channels.interruption import classify_processing_interruption llm = _ClassifierLLM(fail=True) result = await classify_processing_interruption( llm, original_agent="Aqui vai a resposta.", original_client="quanto custa?", supplement_client="oi", ) assert result is False def test_finalizacao_informacional_infer_tipo_pela_fatura(): from app.domain.contas.workflow_actions import _infer_informational_vas_types invoice_detail = { "11999999999": { "Serviços Bundle Inclusos": [{"desc": "EXA Cloud", "value": 0}], "Serviços Contratados de Terceiros": [{"desc": "Netflix Premium", "value": 59.9}], "SVA Detalhe Total": [{"desc": "Aluguel de Filme 2", "value": 19.9}], } } assert _infer_informational_vas_types( ["EXA Cloud", "Netflix Premium", "Aluguel de Filme 2"], invoice_detail ) == {"bundle", "estrategico", "avulso"} assert _infer_informational_vas_types(["exa clod"], invoice_detail) == {"bundle"} def test_finalizacao_informacional_infer_tipo_de_billing_analysis_json(): import json from app.domain.contas.workflow_actions import _infer_informational_vas_types payload = { "currentInvoice": [ {"desc": "Serviços de valor adicionado", "items": [ {"contestable": True, "desc": "Aluguel de Filme 3", "type": "servicos_contratados_de_parceiros"}, {"contestable": False, "desc": "VOD + Canais Abertos", "type": "servicos_contratados_de_parceiros"}, ]}, {"desc": "Streamings", "items": [ {"contestable": False, "desc": "YouTube Premium Mensal", "type": "streaming"} ]}, ] } assert _infer_informational_vas_types( ["YouTube Premium Mensal", "Aluguel de Filme 3"], json.dumps(payload, ensure_ascii=False) ) == {"estrategico", "avulso"} def test_output_guardrail_context_inclui_pedido_atual_para_aoferta(): from app.workflows.agent_graph import AgentWorkflow ctx = AgentWorkflow._output_guardrail_context( { "context": {}, "history": [ {"role": "assistant", "content": "O TIM Music custa R$ 12,90."}, ], "user_text": "Quero cancelar o TIM Music", "mcp_results": [], } ) assert ctx["conversation_history"][-1] == { "role": "user", "content": "Quero cancelar o TIM Music", } assert ctx["history_texts"][-1] == "Quero cancelar o TIM Music" def test_output_guardrail_context_nao_duplica_user_text_ja_no_historico(): from app.workflows.agent_graph import AgentWorkflow ctx = AgentWorkflow._output_guardrail_context( { "context": {}, "history": [{"role": "user", "content": "Quero cancelar o TIM Music"}], "user_text": "Quero cancelar o TIM Music", "mcp_results": [], } ) assert len(ctx["conversation_history"]) == 1