"""Helper utilities for plan correction diff Robot integration tests. Covers: - correction_diff() output in rich, plain, JSON, and YAML formats - PlanError raised when correction attempt not found """ from __future__ import annotations import json import sys from collections.abc import Callable from datetime import UTC, datetime from unittest.mock import MagicMock import yaml from cleveragents.application.services.plan_apply_service import ( PlanApplyService, ) from cleveragents.application.services.plan_lifecycle_service import ( PlanLifecycleService, ) from cleveragents.config.settings import Settings from cleveragents.core.exceptions import PlanError, ValidationError from cleveragents.domain.models.core.correction import ( CorrectionAttemptRecord, CorrectionAttemptState, CorrectionMode, ) from cleveragents.infrastructure.database.repositories import ( CorrectionAttemptNotFoundError, ) def _make_attempt( correction_id: str = "01HXM9B7Z3Q1CORR001AAAAA", plan_id: str = "01HXM8C2ZK4Q7C2B3F2R4PLAN", original_decision_id: str = "01HXM9A1C2Q7W3R5ORIG0001", new_decision_id: str | None = "01HXM9A1C2Q7W3R5NEW00001", mode: CorrectionMode = CorrectionMode.REVERT, state: CorrectionAttemptState = CorrectionAttemptState.COMPLETE, guidance: str = "Use async processing instead", archived_artifacts_path: str | None = None, ) -> CorrectionAttemptRecord: """Build a minimal CorrectionAttemptRecord for testing.""" return CorrectionAttemptRecord( correction_attempt_id=correction_id, plan_id=plan_id, original_decision_id=original_decision_id, new_decision_id=new_decision_id, mode=mode, state=state, guidance=guidance, created_at=datetime(2025, 6, 15, 10, 30, 0, tzinfo=UTC), completed_at=datetime(2025, 6, 15, 10, 31, 0, tzinfo=UTC), archived_artifacts_path=archived_artifacts_path, ) def _make_apply_service_with_mocked_uow( attempt: CorrectionAttemptRecord, ) -> tuple[PlanApplyService, str]: """Create a PlanApplyService with a mocked UoW returning the given attempt.""" settings = Settings() lifecycle = PlanLifecycleService(settings=settings) # Create an action and plan so get_plan() succeeds lifecycle.create_action( name="local/robot-correction-diff-test", description="Robot correction diff test", definition_of_done="Tests pass", strategy_actor="local/planner", execution_actor="local/executor", ) plan = lifecycle.use_action(action_name="local/robot-correction-diff-test") plan_id = plan.identity.plan_id # Override the attempt plan_id to match attempt_with_plan = attempt.model_copy(update={"plan_id": plan_id}) # Build a mock UnitOfWork with transaction context manager mock_ctx = MagicMock() mock_ctx.correction_attempts.get.return_value = attempt_with_plan mock_uow = MagicMock() mock_uow.transaction.return_value.__enter__.return_value = mock_ctx mock_uow.transaction.return_value.__exit__.return_value = False svc = PlanApplyService( lifecycle_service=lifecycle, unit_of_work=mock_uow, ) return svc, plan_id def _correction_diff_rich() -> None: """Test correction_diff() rich format output.""" attempt = _make_attempt() svc, plan_id = _make_apply_service_with_mocked_uow(attempt) output = svc.correction_diff(plan_id, attempt.correction_attempt_id, fmt="rich") assert "Correction Diff" in output, f"Missing 'Correction Diff': {output[:300]}" assert "Comparison" in output, f"Missing 'Comparison': {output[:300]}" assert "Patch Preview" in output, f"Missing 'Patch Preview': {output[:300]}" assert attempt.correction_attempt_id in output, ( f"Correction ID missing: {output[:300]}" ) assert attempt.original_decision_id in output, ( f"Original decision missing: {output[:300]}" ) print("correction-diff-rich-ok") def _correction_diff_plain() -> None: """Test correction_diff() plain format output.""" attempt = _make_attempt() svc, plan_id = _make_apply_service_with_mocked_uow(attempt) output = svc.correction_diff(plan_id, attempt.correction_attempt_id, fmt="plain") assert "Correction Diff" in output, f"Missing 'Correction Diff': {output[:300]}" assert "Comparison" in output, f"Missing 'Comparison': {output[:300]}" assert "Patch Preview" in output, f"Missing 'Patch Preview': {output[:300]}" assert attempt.correction_attempt_id in output, ( f"Correction ID missing: {output[:300]}" ) print("correction-diff-plain-ok") def _correction_diff_json() -> None: """Test correction_diff() JSON format output.""" attempt = _make_attempt() svc, plan_id = _make_apply_service_with_mocked_uow(attempt) output = svc.correction_diff(plan_id, attempt.correction_attempt_id, fmt="json") parsed = json.loads(output) assert "correction_diff" in parsed, f"Missing 'correction_diff' key: {list(parsed)}" assert "comparison" in parsed, f"Missing 'comparison' key: {list(parsed)}" assert "patch_preview" in parsed, f"Missing 'patch_preview' key: {list(parsed)}" assert parsed["correction_diff"]["correction"] == attempt.correction_attempt_id assert parsed["correction_diff"]["mode"] == "revert" print("correction-diff-json-ok") def _correction_diff_yaml() -> None: """Test correction_diff() YAML format output.""" attempt = _make_attempt() svc, plan_id = _make_apply_service_with_mocked_uow(attempt) output = svc.correction_diff(plan_id, attempt.correction_attempt_id, fmt="yaml") parsed = yaml.safe_load(output) assert isinstance(parsed, dict), ( f"Expected dict, got {type(parsed)}: {output[:300]}" ) assert "correction_diff" in parsed, f"Missing 'correction_diff' key: {list(parsed)}" assert "comparison" in parsed, f"Missing 'comparison' key: {list(parsed)}" assert "patch_preview" in parsed, f"Missing 'patch_preview' key: {list(parsed)}" assert parsed["correction_diff"]["correction"] == attempt.correction_attempt_id print("correction-diff-yaml-ok") def _correction_not_found() -> None: """Test correction_diff() raises PlanError when correction not found.""" settings = Settings() lifecycle = PlanLifecycleService(settings=settings) lifecycle.create_action( name="local/robot-correction-not-found", description="Robot correction not found test", definition_of_done="Tests pass", strategy_actor="local/planner", execution_actor="local/executor", ) plan = lifecycle.use_action(action_name="local/robot-correction-not-found") plan_id = plan.identity.plan_id # Mock UoW that raises CorrectionAttemptNotFoundError mock_ctx = MagicMock() mock_ctx.correction_attempts.get.side_effect = CorrectionAttemptNotFoundError( "Not found" ) mock_uow = MagicMock() mock_uow.transaction.return_value.__enter__.return_value = mock_ctx mock_uow.transaction.return_value.__exit__.return_value = False svc = PlanApplyService(lifecycle_service=lifecycle, unit_of_work=mock_uow) try: svc.correction_diff(plan_id, "CORR-NONEXISTENT", fmt="plain") raise AssertionError("Should have raised PlanError") except PlanError as e: assert "not found" in e.message.lower(), f"Unexpected message: {e.message}" print("correction-not-found-ok") def _correction_diff_uow_none() -> None: """Test correction_diff() raises ValidationError when unit_of_work is None.""" settings = Settings() lifecycle = PlanLifecycleService(settings=settings) svc = PlanApplyService(lifecycle_service=lifecycle, unit_of_work=None) try: svc.correction_diff("some-plan-id", "some-correction-id", fmt="plain") raise AssertionError("Should have raised ValidationError") except ValidationError as e: assert "unit_of_work is required" in e.message, ( f"Unexpected message: {e.message}" ) print("correction-diff-uow-none-ok") COMMANDS: dict[str, Callable[[], None]] = { "correction-diff-rich": _correction_diff_rich, "correction-diff-plain": _correction_diff_plain, "correction-diff-json": _correction_diff_json, "correction-diff-yaml": _correction_diff_yaml, "correction-not-found": _correction_not_found, "correction-diff-uow-none": _correction_diff_uow_none, } if __name__ == "__main__": if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} ", file=sys.stderr) print(f"Available: {', '.join(COMMANDS)}", file=sys.stderr) sys.exit(1) cmd = sys.argv[1] func = COMMANDS.get(cmd) if func is None: print(f"Unknown command: {cmd}", file=sys.stderr) sys.exit(1) func()