Files
agent_contas/tests/migration/test_backend_wrapper_contract_parity.py

315 lines
14 KiB
Python

from __future__ import annotations
import asyncio
from types import SimpleNamespace
from typing import Any
from contas_mcp.servers.contas_mcp_server import main as mcp_main
def _result(*, status: str = "COMPLETED", output: dict[str, Any] | None = None,
error: str | None = None, trace: list[dict[str, Any]] | None = None,
error_details: dict[str, Any] | None = None) -> SimpleNamespace:
return SimpleNamespace(
execution_id="exec-test",
status=status,
output=output or {},
error=error,
trace=trace or [],
state={},
error_details=error_details,
model_dump=lambda: {
"execution_id": "exec-test",
"status": status,
"output": output or {},
"error": error,
"trace": trace or [],
"state": {},
"error_details": error_details,
},
)
class _Runtime:
def __init__(self, *results: Any):
self.results = list(results)
self.calls: list[tuple[str, dict[str, Any], str | None]] = []
async def arun(self, name: str, payload: dict[str, Any], execution_id: str | None = None):
self.calls.append((name, dict(payload), execution_id))
if not self.results:
raise AssertionError("resultado não configurado")
return self.results.pop(0)
def test_contestacao_wrapper_preserva_tipo_atendimento_e_contexto_do_turno(monkeypatch):
runtime = _Runtime(_result(output={"abrir_contestacao_cliente": {"success": True}}))
monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime)
args = {
"msisdn": "15981110720",
"subject": "VAS",
"valor": "10",
"message_id": "msg-123",
"customer_message": "Sim",
"session_created_at": "2026-05-05T12:00:00+00:00",
"ura_call_id": "CALL-123",
"channel_id": "URA",
"ani": "11900000000",
"invoice_id": "3000132705",
}
result = asyncio.run(mcp_main._run_workflow("contestacao_tool", args))
assert result["status"] == "COMPLETED"
_, payload, _ = runtime.calls[0]
assert payload["tipo_atendimento"] == "contestacao"
for key in ("message_id", "customer_message", "session_created_at", "ura_call_id", "channel_id", "ani", "invoice_id"):
assert payload[key] == args[key]
def test_contestacao_wrapper_mapeia_falha_tecnica_como_sistemica(monkeypatch):
runtime = _Runtime(_result(status="FAILED", error="gateway indisponivel"))
monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime)
result = asyncio.run(mcp_main._run_workflow("contestacao_tool", {"msisdn": "62999999999", "subject": "VAS"}))
assert result["success"] is False
assert result["erro_sistemico"] is True
assert result["erro"] == "erro_falha_sistema"
assert "contestation_error_description" not in result
def test_contestacao_wrapper_preserva_erro_de_negocio_e_protocolo(monkeypatch):
msg = "Impossível prosseguir, pois não existe os itens informados na fatura questionada."
runtime = _Runtime(_result(
status="FAILED",
error=msg,
output={"registrar_protocolo": {"protocolo_id": "PRT-123"}},
error_details={"body": {"description": "Itens já contestados", "provider": {"errorMessage": msg}}},
))
monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime)
result = asyncio.run(mcp_main._run_workflow("contestacao_tool", {"msisdn": "62999999999", "subject": "VAS"}))
assert result["success"] is False
assert result["erro_sistemico"] is False
assert result["contestation_error_description"] == msg
assert result["mensagem"] == msg
assert result["contestacao_protocol"] == "PRT-123"
def test_invoice_explanation_branch_nao_sem_vas_recomenda_nao_resolvido():
result = mcp_main._result_payload(
_result(trace=[{"node": "finalizar_nao_resolvido"}]),
workflow_name="invoice_explanation",
)
assert result["recomenda_finalizacao"] is True
assert result["status_finalizacao_sugerido"] == "nao_resolvido"
def test_invoice_explanation_branch_devolver_orquestrador_nao_auto_finaliza():
result = mcp_main._result_payload(
_result(trace=[{"node": "devolver_orquestrador"}]),
workflow_name="invoice_explanation",
)
assert "recomenda_finalizacao" not in result
assert "status_finalizacao_sugerido" not in result
def test_pro_rata_branch_devolver_orquestrador_nao_auto_finaliza():
result = mcp_main._result_payload(
_result(trace=[{"node": "devolver_orquestrador"}]),
workflow_name="pro_rata",
)
assert "recomenda_finalizacao" not in result
assert "status_finalizacao_sugerido" not in result
def test_pro_rata_aceite_e_nao_controle_recomendam_resolvido():
for node in ("registrar_aceitou", "registrar_nao_controle"):
result = mcp_main._result_payload(_result(trace=[{"node": node}]), workflow_name="pro_rata")
assert result["recomenda_finalizacao"] is True
assert result["status_finalizacao_sugerido"] == "resolvido"
def test_vas_estrategico_workflow_payload_preserva_tres_itens_e_action_agrupa_duas_linhas():
from app.domain.contas.workflow_actions import build_contas_workflow_actions
class Service:
pass
action = build_contas_workflow_actions(Service()).get("preparar_vas_estrategico")
items = [
{"type": "bundle", "msisdn": "5511999999999", "name": "Netflix"},
{"type": "estrategico", "msisdn": "5511999999999", "name": "Aya E-Books"},
{"type": "estrategico", "msisdn": "5511988888888", "name": "YouTube Premium"},
]
payload = mcp_main._workflow_payload("vas_estrategico", {"msisdn": "5511999999999", "items": items})
assert len(payload["items"]) == 3
result = action(payload, {"input": payload})
assert len(result["linhas"]) == 2
first = result["linhas"][0]
assert first["msisdn"] == "5511999999999"
assert "Netflix" in first["bundle_names"]
assert "Aya E-Books" in first["estrategico_names"]
def test_vas_estrategico_rejeitado_declara_rag_para_cada_servico_estrategico():
from app.domain.contas.workflow_actions import build_contas_workflow_actions
class Service:
pass
action = build_contas_workflow_actions(Service()).get("registrar_atendimento_vas_estrategico")
params = {
"linhas": [
{"msisdn": "5511999999999", "bundle_names": ["Netflix"], "estrategico_names": ["Aya E-Books"]},
{"msisdn": "5511988888888", "bundle_names": [], "estrategico_names": ["YouTube Premium"]},
],
"mensagem_base": "concluido",
}
result = action(params, {"input": {"resposta_usuario": "NAO"}})
assert result["requires_rag"] is True
assert len(result["rag_queries"]) == 2
assert any("Aya E-Books" in q for q in result["rag_queries"])
assert any("YouTube Premium" in q for q in result["rag_queries"])
def test_cancelamento_sem_candidatos_nao_dispara_contestacao(monkeypatch):
cancel = _result(
output={"cancelar_vas_avulso": {
"success": True,
"cancelados": [{"subject": "VAS X", "success": True}],
"contestation_candidates": [],
"protocolos_por_linha": [{"protocolo_id": "PRT-CANCEL-1"}],
}}
)
runtime = _Runtime(cancel)
monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime)
result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({
"msisdn": "11999999999", "subject": "VAS X", "items": [{"msisdn": "11999999999", "name": "VAS X"}],
}))
assert len(runtime.calls) == 1
assert result["success"] is True
assert result["cancelamento_vas_protocol"] == "PRT-CANCEL-1"
def test_cancelamento_falha_operacional_com_candidato_dispara_contestacao(monkeypatch):
cancel = _result(output={"cancelar_vas_avulso": {
"success": False,
"cancelados": [],
"nao_cancelados": [{"subject": "VAS X", "success": False, "error": "block_vas_failed"}],
"contestation_candidates": [{"subject": "VAS X", "msisdn": "11999999999", "value": "10.00", "eligible_for_contestation": True}],
"protocolos_por_linha": [{"protocolo_id": "PRT-CANCEL-1"}],
}})
contest = _result(output={
"registrar_protocolo": {"protocolo_id": "PRT-CONT-1"},
"abrir_contestacao_cliente": {"success": True, "items_response": [{"status": "CRIAR", "itemName": "VAS X"}]},
"enviar_sms": {"success": True},
})
runtime = _Runtime(cancel, contest)
monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime)
result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({
"msisdn": "11999999999", "subject": "VAS X", "items": [{"msisdn": "11999999999", "name": "VAS X", "value": "10.00"}],
"complete_invoices_payload": {"billingProfile": {"customer": {"document": "123", "customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}]},
}))
assert [c[0] for c in runtime.calls] == ["cancelamento_vas_avulso", "contestacao_tool"]
assert result["contestacao_protocol"] == "PRT-CONT-1"
assert result["cancelamento_vas_protocol"] == "PRT-CANCEL-1"
def test_cancelamento_sms_falho_preserva_sucesso_e_flag(monkeypatch):
cancel = _result(output={"cancelar_vas_avulso": {
"success": True,
"cancelados": [{"subject": "VAS X", "success": True}],
"contestation_candidates": [{"subject": "VAS X", "msisdn": "11999999999", "value": "10.00"}],
"protocolos_por_linha": [{"protocolo_id": "PRT-CANCEL-1"}],
}})
contest = _result(output={
"registrar_protocolo": {"protocolo_id": "PRT-CONT-1"},
"abrir_contestacao_cliente": {"success": True, "items_response": [{"status": "CRIAR", "itemName": "VAS X"}]},
"enviar_sms": {"success": False, "error": "sms indisponível"},
})
runtime = _Runtime(cancel, contest)
monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime)
result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({
"msisdn": "11999999999", "subject": "VAS X", "items": [{"msisdn": "11999999999", "name": "VAS X", "value": "10.00"}],
"complete_invoices_payload": {"billingProfile": {"customer": {"document": "123", "customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}]},
}))
assert result["success"] is True
assert result["sms_not_send_error"] is True
def test_cancelamento_aceita_cpf_como_alias_social_sec_no(monkeypatch):
cancel = _result(output={"cancelar_vas_avulso": {
"success": True,
"cancelados": [{"servico": "VAS X"}],
"contestation_candidates": [{"subject": "VAS X", "msisdn": "11999999999", "value": "10.00"}],
"protocolos_por_linha": [{"protocolo_id": "PRT-CANCEL-1"}],
}})
contest = _result(output={
"registrar_protocolo": {"protocolo_id": "PRT-CONT-1"},
"abrir_contestacao_cliente": {"success": True},
})
runtime = _Runtime(cancel, contest)
monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime)
asyncio.run(mcp_main._run_cancelamento_com_contestacao({
"msisdn": "11999999999", "cpf": "123.456.789-01", "subject": "VAS X",
"items": [{"msisdn": "11999999999", "name": "VAS X", "value": "10.00"}],
"complete_invoices_payload": {"billingProfile": {"customer": {"customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}]},
}))
_, cancel_payload, _ = runtime.calls[0]
_, contest_payload, _ = runtime.calls[1]
assert cancel_payload["social_sec_no"] == "12345678901"
assert contest_payload["social_sec_no"] == "12345678901"
def test_cancelamento_falha_workflow_preserva_output_parcial_nao_cancelados(monkeypatch):
failed = _result(
status="FAILED",
error="Falha técnica ao processar serviços VAS no IU.",
output={"cancelar_vas_avulso": {
"cancelados": [],
"nao_cancelados": [{"msisdn": "11964460192", "servico": "VOD + Canais Abertos", "erro": "Falha no IU", "motivo": "query_vas_failed"}],
"nao_encontrados": [],
}},
)
runtime = _Runtime(failed)
monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime)
result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({"msisdn": "11964460192", "subject": "VOD + Canais Abertos"}))
assert result["status"] == "FAILED"
assert result["output"]["cancelar_vas_avulso"]["nao_cancelados"][0]["motivo"] == "query_vas_failed"
def test_cancelamento_aceita_alias_itens_para_contestacao(monkeypatch):
cancel = _result(output={"cancelar_vas_avulso": {
"success": False,
"cancelados": [],
"itens_para_contestacao": [{"servico": "Inexistente", "msisdn": "11999999999", "valor": "14,99", "iu_reason": "service_not_found"}],
"nao_encontrados": [{"servico": "Inexistente"}],
}})
contest = _result(output={
"registrar_protocolo": {"protocolo_id": "PRT-CANDIDATO"},
"abrir_contestacao_cliente": {"success": True, "items_response": [{"status": "INICIADA", "itemName": "Inexistente"}]},
})
runtime = _Runtime(cancel, contest)
monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime)
result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({
"msisdn": "11999999999", "subject": "Inexistente", "items": [{"msisdn": "11999999999", "name": "Inexistente", "value": "14,99"}],
"complete_invoices_payload": {"billingProfile": {"customer": {"document": "123", "customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}]},
}))
assert [c[0] for c in runtime.calls] == ["cancelamento_vas_avulso", "contestacao_tool"]
assert result["contestacao_protocol"] == "PRT-CANDIDATO"
def test_invoice_explanation_service_failure_preserva_fraseologia_sem_auto_finalize():
from contas_mcp.servers.contas_mcp_server.main import _result_payload
result = {
"execution_id": "exec-ie-falha",
"workflow_name": "invoice_explanation",
"workflow_version": 2,
"status": "COMPLETED",
"output": {"resposta_falha_servico": {"success": False, "service_failed": True, "mensagem": "texto incorreto"}},
"trace": [{"node": "resposta_falha_servico"}],
"error": None,
}
payload = _result_payload(result, workflow_name="invoice_explanation")
assert payload["success"] is False
assert payload["service_failed"] is True
assert payload["auto_finalize_on_failure"] is False
assert payload["mensagem"] == "Para continuar com a sua solicitação, aguarde um instante."