import os import pytest os.environ.setdefault("USE_MOCK_LLM", "true") from agent_framework.guardrails.calibrated import ausencia_oferta_proativa from agent_framework.guardrails.calibrated._compat import RailResult from agent_framework.guardrails.calibrated.prompts.ausencia_oferta_proativa import ( build_aoferta_prompt, ) # --------------------------------------------------------------------------- # # Contrato de saída (2026-08-05): {allowed, reason}, reason vazio se allowed # # --------------------------------------------------------------------------- # def test_prompt_aoferta_nao_pede_label(): # O `label` nunca existiu no prompt real (só no mock); o contrato é # {allowed, reason}, como o FRASEOLOGIA. prompt = build_aoferta_prompt("Gostaria de cancelar esse serviço?") assert '"label"' not in prompt assert '"allowed": true ou false' in prompt def test_prompt_aoferta_exige_reason_vazio_quando_allowed(): prompt = build_aoferta_prompt("Gostaria de cancelar esse serviço?") assert "se allowed=true: string vazia" in prompt assert "se allowed=true: explicacao curta" not in prompt def test_prompt_aoferta_injeta_texto_e_contexto(): prompt = build_aoferta_prompt("posso cancelar?", "\nHistorico:\n[user] oi\n") assert "posso cancelar?" in prompt assert "[user] oi" in prompt @pytest.fixture(autouse=True) def _force_mock(monkeypatch): monkeypatch.setenv("USE_MOCK_LLM", "true") def test_resposta_neutra_eh_permitida(): r = ausencia_oferta_proativa("Posso te ajudar com mais alguma coisa?") assert isinstance(r, RailResult) assert r.allowed is True assert r.code == "AOFERTA" assert r.mechanism == "llm_supervisor" assert r.data is not None assert r.data.get("label") == "OFERTA_OK" def test_oferta_proativa_indevida_bloqueia(): r = ausencia_oferta_proativa( "Ja que esta cancelando um, quer aproveitar e cancelar os outros?", ) assert r.allowed is False assert r.code == "AOFERTA" assert r.mechanism == "llm_supervisor" assert r.data.get("label") == "OFERTA_PROATIVA_INDEVIDA" def test_padrao_aproveita_e_bloqueia(): r = ausencia_oferta_proativa("Quer aproveitar e cancelar os outros?") assert r.allowed is False assert r.code == "AOFERTA" def test_confirmacao_cancelamento_apos_nao_reconhecimento_eh_permitida(): # Cliente nao-reconheceu os servicos em foco ("Nao contratei esses # servicos") e o agente confirma o cancelamento dos MESMOS itens. Per # classificador v9 + orquestrador, isso e action_request -> a confirmacao # deve ser OFERTA_OK, nao oferta proativa. # # NOTA: em USE_MOCK_LLM=true o mock decide AOFERTA por string-match no texto # do agente (sem gatilho de upsell -> OFERTA_OK), ignorando o contexto. Este # teste documenta a intencao; a validacao real do prompt e via LLM/cenarios # (automated_conversation_test cy0004/cy0005/cy0008). r = ausencia_oferta_proativa( "Podemos seguir com o cancelamento dos serviços VOD + Canais Abertos, " "no valor de R$ 19,90, e Aluguel de Filme 1, no valor de R$ 19,90, " "e providenciar a retirada dos valores da fatura, você confirma?", ) assert r.allowed is True assert r.code == "AOFERTA" assert r.data.get("label") == "OFERTA_OK" def test_upsell_continua_bloqueando_apos_mudanca(): # Contraprova: a mudanca para reconhecer nao-reconhecimento NAO pode liberar # marcadores de upsell explicitos. r = ausencia_oferta_proativa( "Ja que esta cancelando esse, que tal tambem cancelar o outro servico?", ) assert r.allowed is False assert r.code == "AOFERTA" assert r.data.get("label") == "OFERTA_PROATIVA_INDEVIDA" def test_prompt_aoferta_confirmacao_nao_exige_repetir_justificativa(): prompt = build_aoferta_prompt( "Você confirma cancelar o TIM Music?", "\nHistorico da conversa:\n[user] Quero cancelar o TIM Music\n", ) assert "A confirmacao NAO precisa repetir a justificativa" in prompt assert "o pedido transacional anterior basta" in prompt