"""Tests unitaires pour la boucle ORA (observe→raisonne→agit).""" import pytest from unittest.mock import MagicMock, patch from core.execution.observe_reason_act import ( ORALoop, Observation, Decision, VerificationResult, LoopResult ) class TestORALoopInit: def test_default_params(self): loop = ORALoop() assert loop.max_retries == 2 assert loop.max_steps == 50 assert loop.verify_level == 'auto' def test_custom_params(self): loop = ORALoop(max_retries=5, max_steps=10, verify_level='phash') assert loop.max_retries == 5 assert loop.max_steps == 10 assert loop.verify_level == 'phash' class TestDecision: def test_click_decision(self): d = Decision( action='click', target='Enregistrer', value='', reasoning='Bouton visible', expected_after='Fichier sauvegardé', confidence=0.95 ) assert d.action == 'click' assert d.done == False def test_done_decision(self): d = Decision( action='done', target='', value='', reasoning='Objectif atteint', expected_after='', confidence=1.0, done=True ) assert d.done == True class TestReasonWorkflowStep: def test_click_anchor_step(self): loop = ORALoop() obs = MagicMock() step = { 'action_type': 'click_anchor', 'label': 'Clic sur Demo', 'visual_anchor': { 'target_text': 'Demo', 'screenshot': 'base64data', 'bounding_box': {'x': 100, 'y': 200, 'width': 50, 'height': 30} } } decision = loop.reason_workflow_step(step, obs) assert decision.action == 'click' assert decision.target == 'Demo' def test_type_text_step(self): loop = ORALoop() obs = MagicMock() step = { 'action_type': 'type_text', 'label': 'Saisir URL', 'parameters': {'text': 'https://youtube.com'} } decision = loop.reason_workflow_step(step, obs) assert decision.action == 'type' assert decision.value == 'https://youtube.com' def test_keyboard_shortcut_step(self): loop = ORALoop() obs = MagicMock() step = { 'action_type': 'keyboard_shortcut', 'label': 'Ctrl+S', 'parameters': {'keys': ['ctrl', 's']} } decision = loop.reason_workflow_step(step, obs) assert decision.action == 'hotkey' def test_wait_step(self): loop = ORALoop() obs = MagicMock() step = { 'action_type': 'wait_for_anchor', 'label': 'Attente', 'parameters': {'timeout_ms': 3000} } decision = loop.reason_workflow_step(step, obs) assert decision.action == 'wait' def test_unknown_step_passthrough(self): loop = ORALoop() obs = MagicMock() step = {'action_type': 'custom_action', 'label': 'Action custom'} decision = loop.reason_workflow_step(step, obs) assert decision.action == 'passthrough' class TestVerify: def test_verify_none_mode(self): loop = ORALoop(verify_level='none') pre = MagicMock() post = MagicMock() decision = Decision('click', 'btn', '', '', '', 0.9) result = loop.verify(pre, post, decision) assert result.success == True def test_verify_wait_action(self): loop = ORALoop(verify_level='phash') pre = MagicMock() post = MagicMock() decision = Decision('wait', '', '', '', '', 0.9) result = loop.verify(pre, post, decision) assert result.success == True def test_verify_done_action(self): loop = ORALoop() pre = MagicMock() post = MagicMock() decision = Decision('done', '', '', '', '', 1.0, done=True) result = loop.verify(pre, post, decision) assert result.success == True class TestRunWorkflow: def test_empty_workflow(self): loop = ORALoop() result = loop.run_workflow([]) assert result.success == True assert result.steps_completed == 0 def test_too_many_steps(self): loop = ORALoop(max_steps=5) steps = [{'action_type': 'wait', 'parameters': {}} for _ in range(10)] result = loop.run_workflow(steps) assert result.success == False assert 'max_steps' in result.reason.lower() or result.steps_completed <= 5 class TestRunInstruction: def test_has_method(self): loop = ORALoop() assert hasattr(loop, 'run_instruction') assert callable(loop.run_instruction) def test_has_reason_instruction(self): loop = ORALoop() assert hasattr(loop, 'reason_instruction') assert callable(loop.reason_instruction)