# test_command.py import pytest from falyx.action import Action, ActionGroup, ChainedAction from falyx.command import Command from falyx.execution_registry import ExecutionRegistry as er from falyx.io_action import BaseIOAction from falyx.retry import RetryPolicy asyncio_default_fixture_loop_scope = "function" # --- Fixtures --- @pytest.fixture(autouse=True) def clean_registry(): er.clear() yield er.clear() # --- Dummy Action --- async def dummy_action(): return "ok" # --- Dummy IO Action --- class DummyInputAction(BaseIOAction): async def _run(self, *args, **kwargs): return "needs input" async def preview(self, parent=None): pass # --- Tests --- @pytest.mark.asyncio async def test_command_creation(): """Test if Command can be created with a callable.""" action = Action("test_action", dummy_action) cmd = Command(key="TEST", description="Test Command", action=action) assert cmd.key == "TEST" assert cmd.description == "Test Command" assert cmd.action == action result = await cmd() assert result == "ok" assert cmd.result == "ok" def test_command_str(): """Test if Command string representation is correct.""" action = Action("test_action", dummy_action) cmd = Command(key="TEST", description="Test Command", action=action) assert ( str(cmd) == "Command(key='TEST', description='Test Command' action='Action(name='test_action', action=dummy_action, args=(), kwargs={}, retry=False)')" ) @pytest.mark.parametrize( "action_factory, expected_requires_input", [ (lambda: Action(name="normal", action=dummy_action), False), (lambda: DummyInputAction(name="io"), True), ( lambda: ChainedAction(name="chain", actions=[DummyInputAction(name="io")]), True, ), ( lambda: ActionGroup(name="group", actions=[DummyInputAction(name="io")]), True, ), ], ) def test_command_requires_input_detection(action_factory, expected_requires_input): action = action_factory() cmd = Command(key="TEST", description="Test Command", action=action) assert cmd.requires_input == expected_requires_input if expected_requires_input: assert cmd.hidden is True else: assert cmd.hidden is False def test_requires_input_flag_detected_for_baseioaction(): """Command should automatically detect requires_input=True for BaseIOAction.""" cmd = Command( key="X", description="Echo input", action=DummyInputAction(name="dummy"), ) assert cmd.requires_input is True assert cmd.hidden is True def test_requires_input_manual_override(): """Command manually set requires_input=False should not auto-hide.""" cmd = Command( key="Y", description="Custom input command", action=DummyInputAction(name="dummy"), requires_input=False, ) assert cmd.requires_input is False assert cmd.hidden is False def test_default_command_does_not_require_input(): """Normal Command without IO Action should not require input.""" cmd = Command( key="Z", description="Simple action", action=lambda: 42, ) assert cmd.requires_input is False assert cmd.hidden is False def test_chain_requires_input(): """If first action in a chain requires input, the command should require input.""" chain = ChainedAction( name="ChainWithInput", actions=[ DummyInputAction(name="dummy"), Action(name="action1", action=lambda: 1), ], ) cmd = Command( key="A", description="Chain with input", action=chain, ) assert cmd.requires_input is True assert cmd.hidden is True def test_group_requires_input(): """If any action in a group requires input, the command should require input.""" group = ActionGroup( name="GroupWithInput", actions=[ Action(name="action1", action=lambda: 1), DummyInputAction(name="dummy"), ], ) cmd = Command( key="B", description="Group with input", action=group, ) assert cmd.requires_input is True assert cmd.hidden is True def test_enable_retry(): """Command should enable retry if action is an Action and retry is set to True.""" cmd = Command( key="A", description="Retry action", action=Action( name="retry_action", action=lambda: 42, ), retry=True, ) assert cmd.retry is True assert cmd.action.retry_policy.enabled is True def test_enable_retry_with_retry_policy(): """Command should enable retry if action is an Action and retry_policy is set.""" retry_policy = RetryPolicy( max_retries=3, delay=1, backoff=2, enabled=True, ) cmd = Command( key="B", description="Retry action with policy", action=Action( name="retry_action_with_policy", action=lambda: 42, ), retry_policy=retry_policy, ) assert cmd.action.retry_policy.enabled is True assert cmd.action.retry_policy == retry_policy def test_enable_retry_not_action(): """Command should not enable retry if action is not an Action.""" cmd = Command( key="C", description="Retry action", action=DummyInputAction, retry=True, ) assert cmd.retry is True with pytest.raises(Exception) as exc_info: assert cmd.action.retry_policy.enabled is False assert "'function' object has no attribute 'retry_policy'" in str(exc_info.value) def test_chain_retry_all(): """retry_all should retry all Actions inside a ChainedAction recursively.""" chain = ChainedAction( name="ChainWithRetry", actions=[ Action(name="action1", action=lambda: 1), Action(name="action2", action=lambda: 2), ], ) cmd = Command( key="D", description="Chain with retry", action=chain, retry_all=True, ) assert cmd.retry_all is True assert cmd.retry_policy.enabled is True assert chain.actions[0].retry_policy.enabled is True assert chain.actions[1].retry_policy.enabled is True def test_chain_retry_all_not_base_action(): """retry_all should not be set if action is not a ChainedAction.""" cmd = Command( key="E", description="Chain with retry", action=DummyInputAction, retry_all=True, ) assert cmd.retry_all is True with pytest.raises(Exception) as exc_info: assert cmd.action.retry_policy.enabled is False assert "'function' object has no attribute 'retry_policy'" in str(exc_info.value) @pytest.mark.asyncio async def test_command_exception_handling(): """Test if Command handles exceptions correctly.""" async def bad_action(): raise ZeroDivisionError("This is a test exception") cmd = Command(key="TEST", description="Test Command", action=bad_action) with pytest.raises(ZeroDivisionError): await cmd() assert cmd.result is None assert isinstance(cmd._context.exception, ZeroDivisionError) def test_command_bad_action(): """Test if Command raises an exception when action is not callable.""" with pytest.raises(TypeError) as exc_info: Command(key="TEST", description="Test Command", action="not_callable") assert str(exc_info.value) == "Action must be a callable or an instance of BaseAction"