from __future__ import annotations import json import pytest from agent import FailureDetail, FailureDisposition, ToolExecutionError, ToolResult def test_failure_detail_is_bounded_json_safe_and_round_trips() -> None: failure = FailureDetail( code="INPUT_SCOPE_MISMATCH", message="x" * 3_000, disposition=FailureDisposition.REPLAN_TASK, source_tool="save_candidate", details={"task_id": "task-1", "scope": ["paragraph", 3]}, ) assert len(failure.message) == 2_000 assert FailureDetail.from_dict(failure.to_dict()) == failure assert failure.fingerprint() == FailureDetail( code="INPUT_SCOPE_MISMATCH", message="different wording", disposition=FailureDisposition.REPLAN_TASK, source_tool="save_candidate", details={"task_id": "task-2", "scope": ["paragraph", 3]}, ).fingerprint() def test_failure_detail_rejects_non_json_details() -> None: with pytest.raises(ValueError, match="JSON values"): FailureDetail( code="INVALID", message="invalid", disposition=FailureDisposition.RETRY_CALL, details={"value": object()}, ) def test_tool_result_exposes_structured_failure_to_model() -> None: failure = FailureDetail( code="REJECTED", message="retry with another value", disposition=FailureDisposition.RETRY_CALL, source_tool="sample", ) result = ToolResult(title="rejected", output="context", failure=failure) payload = json.loads(result.to_llm_message()) assert payload["failure"] == failure.to_dict() assert payload["output"] == "context" assert ToolExecutionError(failure).failure == failure