Files
agent_contas/tests/migration/test_adversarial_parity.py
2026-08-21 10:44:34 -03:00

396 lines
13 KiB
Python

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