diff --git a/src/adcp/decisioning/dispatch.py b/src/adcp/decisioning/dispatch.py index faad9898d..e643021de 100644 --- a/src/adcp/decisioning/dispatch.py +++ b/src/adcp/decisioning/dispatch.py @@ -700,7 +700,7 @@ def _internal_error_details(exc: BaseException) -> dict[str, Any]: except Exception: # Defensive — never let a narrowing bug 500 the wire. # The exception type still lets adopters triage via server logs. - pass + return details return details diff --git a/tests/test_decisioning_dispatch.py b/tests/test_decisioning_dispatch.py index 357da99b0..01f48f4bf 100644 --- a/tests/test_decisioning_dispatch.py +++ b/tests/test_decisioning_dispatch.py @@ -879,6 +879,40 @@ async def get_products(self, req, ctx): assert "eyJhbGciOiJIUzI1NiJ9" not in str(exc_info.value.details) +def test_internal_error_details_survive_a_narrowing_failure_without_raw_values(monkeypatch): + from pydantic import ValidationError + + from adcp.decisioning.dispatch import _internal_error_details + from adcp.types import error_narrowing + + class _InvalidResponse(BaseModel): + count: int + + with pytest.raises(ValidationError) as captured: + _InvalidResponse.model_validate({"count": "raw-input-marker"}) + observed = [] + + def broken_narrowing(errors): + observed.extend(errors) + yield {"msg": "partial-narrowing-marker"} + raise RuntimeError("raw-narrowing-failure-marker") + + monkeypatch.setattr(error_narrowing, "narrow_union_errors", broken_narrowing) + details = _internal_error_details(captured.value) + assert observed and all("input" not in item and "ctx" not in item for item in observed) + assert details == {"caused_by": {"type": "ValidationError"}} + # A partial generator result and either raw value must not enter the error + # response; the secondary narrowing failure must not replace the original. + assert all( + marker not in str(details) + for marker in ( + "raw-input-marker", + "partial-narrowing-marker", + "raw-narrowing-failure-marker", + ) + ) + + @pytest.mark.asyncio async def test_invoke_validation_error_surfaces_narrowed_field_paths( executor: ThreadPoolExecutor,