diff --git a/apps/api/src/services/awooop_ansible_check_mode_service.py b/apps/api/src/services/awooop_ansible_check_mode_service.py index 0b20c0255..6abba3277 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -2460,10 +2460,20 @@ async def _record_incident_terminal_disposition( UPDATE incidents SET status = CAST(:incident_status AS incidentstatus), outcome = CAST( - coalesce(CAST(outcome AS jsonb), '{}'::jsonb) - || jsonb_build_object( - 'automation_terminal', - CAST(:terminal_payload AS jsonb) + jsonb_set( + CASE + WHEN outcome IS NULL THEN '{}'::jsonb + WHEN jsonb_typeof(CAST(outcome AS jsonb)) + = 'object' + THEN CAST(outcome AS jsonb) + ELSE jsonb_build_object( + 'legacy_outcome', + CAST(outcome AS jsonb) + ) + END, + '{automation_terminal}', + CAST(:terminal_payload AS jsonb), + true ) AS json ), diff --git a/apps/api/tests/test_ansible_verified_closure.py b/apps/api/tests/test_ansible_verified_closure.py index 316d688ed..9148dd317 100644 --- a/apps/api/tests/test_ansible_verified_closure.py +++ b/apps/api/tests/test_ansible_verified_closure.py @@ -1,5 +1,6 @@ from __future__ import annotations +import inspect from unittest.mock import AsyncMock import pytest @@ -56,6 +57,15 @@ def test_runtime_stage_ids_only_accepts_durable_receipts() -> None: assert stage_ids == {"mcp_context"} +def test_incident_terminal_writer_normalizes_legacy_non_object_outcome() -> None: + source = inspect.getsource(service._record_incident_terminal_disposition) + + assert "jsonb_typeof(CAST(outcome AS jsonb))" in source + assert "jsonb_set(" in source + assert "'legacy_outcome'" in source + assert "'{automation_terminal}'" in source + + @pytest.mark.asyncio async def test_closure_refuses_to_resolve_when_any_receipt_is_missing( monkeypatch: pytest.MonkeyPatch,