from __future__ import annotations from typing import Any from unittest.mock import MagicMock, patch from agents.collab_runner import CollaborativeTaskRunner, CollaborativeTaskRunnerConfig from agents.config import CollaborationConfig from agents.models import Observation, ReflectionAction, ReflectionOutcome, VerificationVerdict from core.models import Bounds, Scene, SceneElement, Task from runtime.executor import StepResult from runtime.planner import PlannedStep def _scene() -> Scene: return Scene( width=1080, height=1920, elements=[SceneElement(id="btn1", type="button", bounds=Bounds(10, 20, 100, 50), text="OK")], ) def _task() -> Task: return Task(goal="Test goal", device_id="dev1") def _planned_step() -> PlannedStep: return PlannedStep(action="describe_screen", description="Observe", args={}) def _success_step_result() -> StepResult: return StepResult(step=_planned_step(), success=True, attempts=1, result="ok") def _observation() -> Observation: return Observation(scene_summary="Screen 1080x1920") def _achieved_verdict() -> VerificationVerdict: return VerificationVerdict(result="achieved", reasoning="Effect observed.") def _not_achieved_verdict() -> VerificationVerdict: return VerificationVerdict(result="not_achieved", reasoning="No change.") def _replan_outcome() -> ReflectionOutcome: return ReflectionOutcome(replan=True, reasoning="No recovery.") def _recovery_outcome() -> ReflectionOutcome: return ReflectionOutcome( replan=False, action=ReflectionAction(action="swipe", description="Scroll", args={"direction": "up"}), reasoning="Try scrolling.", ) def test_completes_without_verification_failure() -> None: """Task completes normally when verifier reports achieved.""" planner = MagicMock() planner.plan.return_value = [_planned_step()] planner.goal_reached.return_value = True executor = MagicMock() executor.execute.return_value = _success_step_result() observer = MagicMock() observer.observe.return_value = _observation() verifier = MagicMock() verifier.verify.return_value = _achieved_verdict() reflector = MagicMock() runner = CollaborativeTaskRunner( planner=planner, executor=executor, observer=observer, verifier=verifier, reflector=reflector, config=CollaborativeTaskRunnerConfig(max_steps=5), collaboration_config=CollaborationConfig(enabled=True, max_recovery_attempts=3), ) with patch("tools.describe_screen.describe_screen", return_value=_scene()): task = runner.run(_task()) assert task.status == "completed" reflector.reflect.assert_not_called() def test_recovers_via_reflector_action() -> None: """Task recovers when reflector proposes a distinct action.""" call_count = 0 def verifier_side_effect(**kwargs: Any) -> VerificationVerdict: nonlocal call_count call_count += 1 if call_count == 1: return _not_achieved_verdict() return _achieved_verdict() planner = MagicMock() planner.plan.return_value = [_planned_step()] planner.goal_reached.side_effect = [False, True] executor = MagicMock() executor.execute.return_value = _success_step_result() observer = MagicMock() observer.observe.return_value = _observation() verifier = MagicMock() verifier.verify.side_effect = verifier_side_effect reflector = MagicMock() reflector.reflect.return_value = _recovery_outcome() runner = CollaborativeTaskRunner( planner=planner, executor=executor, observer=observer, verifier=verifier, reflector=reflector, config=CollaborativeTaskRunnerConfig(max_steps=5), collaboration_config=CollaborationConfig(enabled=True, max_recovery_attempts=3), ) with patch("tools.describe_screen.describe_screen", return_value=_scene()): task = runner.run(_task()) assert task.status == "completed" reflector.reflect.assert_called_once() executor.execute.assert_called() # recovery action also executed def test_exhausts_recovery_ceiling() -> None: """Task fails when reflection-recovery ceiling is reached.""" planner = MagicMock() planner.plan.return_value = [_planned_step()] planner.goal_reached.return_value = False executor = MagicMock() executor.execute.return_value = _success_step_result() observer = MagicMock() observer.observe.return_value = _observation() verifier = MagicMock() verifier.verify.return_value = _not_achieved_verdict() reflector = MagicMock() reflector.reflect.return_value = _replan_outcome() runner = CollaborativeTaskRunner( planner=planner, executor=executor, observer=observer, verifier=verifier, reflector=reflector, config=CollaborativeTaskRunnerConfig(max_steps=10, max_recovery_attempts=2), collaboration_config=CollaborationConfig(enabled=True, max_recovery_attempts=2), ) with patch("tools.describe_screen.describe_screen", return_value=_scene()): task = runner.run(_task()) assert task.status == "failed" assert "ceiling" in (task.failure_reason or "").lower() def test_disabled_collaboration_runs_plain() -> None: """When collaboration is disabled, delegates to plain TaskRunner.""" planner = MagicMock() executor = MagicMock() mock_task_runner = MagicMock() expected_task = _task() expected_task.status = "completed" mock_task_runner.run.return_value = expected_task runner = CollaborativeTaskRunner( planner=planner, executor=executor, task_runner=mock_task_runner, collaboration_config=CollaborationConfig(enabled=False), ) result = runner.run(_task()) assert result.status == "completed" mock_task_runner.run.assert_called_once()