Projeto do Agent Contas ORACLE
This commit is contained in:
314
tests/migration/test_backend_wrapper_contract_parity.py
Normal file
314
tests/migration/test_backend_wrapper_contract_parity.py
Normal file
@@ -0,0 +1,314 @@
|
||||
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."
|
||||
Reference in New Issue
Block a user