"""Tests del CoreClient: cómo mapea respuestas HTTP del core a dicts. Importante para el dashboard: una ejecución correcta (``AgentExecution`` serializado) incluye SIEMPRE el campo ``error`` (``str | None``). El wrapper que el cliente añade ante un 4xx debe usar una clave distinta (``api_error``) para no colisionar con ese campo; si usara ``error``, las páginas no podrían distinguir "el agente terminó sin error" de "la API devolvió 404/409/422". """ from __future__ import annotations from collections.abc import Callable import httpx import pytest from forja_dashboard.client import CoreClient def _client_with(handler: Callable[[httpx.Request], httpx.Response]) -> CoreClient: client = CoreClient(base_url="http://test") client._client.close() client._client = httpx.Client(base_url="http://test", transport=httpx.MockTransport(handler)) return client def test_post_2xx_devuelve_el_cuerpo_verbatim_incluido_error_none() -> None: """Un AgentExecution con ``error=None`` se devuelve tal cual, sin envolver.""" execution = { "trace_id": "11111111-1111-1111-1111-111111111111", "status": "completed", "error": None, "violations": [], "decision_path": [], } def handler(_: httpx.Request) -> httpx.Response: return httpx.Response(200, json=execution) result = _client_with(handler).invoke_agent("incident_analyzer", {"input": "x"}) assert result == execution assert "api_error" not in result @pytest.mark.parametrize("status", [404, 409, 422]) def test_post_4xx_envuelve_el_detalle_en_api_error(status: int) -> None: def handler(_: httpx.Request) -> httpx.Response: return httpx.Response(status, json={"detail": "boom"}) result = _client_with(handler).invoke_agent("incident_analyzer", {"input": "y"}) assert result == {"api_error": {"detail": "boom"}} def test_post_5xx_propaga_la_excepcion() -> None: def handler(_: httpx.Request) -> httpx.Response: return httpx.Response(500, json={"detail": "kaput"}) with pytest.raises(httpx.HTTPStatusError): _client_with(handler).invoke_agent("incident_analyzer", {"input": "z"})