import asyncio import pytest from falyx.action import Action, ActionGroup, ChainedAction, FallbackAction from falyx.context import ExecutionContext from falyx.execution_registry import ExecutionRegistry as er from falyx.hook_manager import HookType # --- Fixtures --- @pytest.fixture(autouse=True) def clean_registry(): er.clear() yield er.clear() # --- Stress Tests --- @pytest.mark.asyncio async def test_action_group_partial_failure(): async def succeed(): return "ok" async def fail(): raise ValueError("oops") group = ActionGroup( name="partial_group", actions=[ Action(name="succeed_action", action=succeed), Action(name="fail_action", action=fail), ], ) with pytest.raises(Exception) as exc_info: await group() assert "fail_action" in str(exc_info.value) @pytest.mark.asyncio async def test_chained_action_with_nested_group(): group = ActionGroup( name="nested_group", actions=[ Action( name="g1", action=lambda last_result: f"{last_result} + 10", inject_last_result=True, ), Action( name="g2", action=lambda last_result: f"{last_result} + 20", inject_last_result=True, ), ], ) chain = ChainedAction( name="chain_with_group", actions=[ "start", group, ], auto_inject=True, return_list=True, ) result = await chain() assert result[0] == "start" result_dict = dict(result[1]) assert result_dict == {"g1": "start + 10", "g2": "start + 20"} @pytest.mark.asyncio async def test_chained_action_with_error_mid_fallback(): async def ok(): return 1 async def fail(): raise RuntimeError("bad") chain = ChainedAction( name="group_with_fallback", actions=[ Action(name="ok", action=ok), Action(name="fail", action=fail), FallbackAction(fallback="recovered"), ], return_list=True, ) result = await chain() assert result == [1, None, "recovered"] @pytest.mark.asyncio async def test_chained_action_double_fallback(): async def fetch_data(): return None async def validate_data(last_result): if last_result is None: raise ValueError("No data!") return last_result async def enrich(last_result): return f"Enriched: {last_result}" chain = ChainedAction( name="fallback_chain", actions=[ Action(name="Fetch", action=fetch_data), FallbackAction(fallback="default1"), Action(name="Validate", action=validate_data), FallbackAction(fallback="default2"), Action(name="Enrich", action=enrich), ], auto_inject=True, return_list=True, ) result = await chain() assert result == [None, "default1", "default1", "default1", "Enriched: default1"] @pytest.mark.asyncio async def test_large_chain_stress(): chain = ChainedAction( name="large_chain", actions=[ Action( name=f"a{i}", action=lambda last_result: ( last_result + 1 if last_result is not None else 0 ), inject_last_result=True, ) for i in range(50) ], auto_inject=True, ) result = await chain() assert result == 49 # Start from 0 and add 1 fifty times @pytest.mark.asyncio async def test_nested_chain_inside_group(): inner_chain = ChainedAction( name="inner", actions=[ 1, Action( name="a", action=lambda last_result: last_result + 1, inject_last_result=True, ), Action( name="b", action=lambda last_result: last_result + 2, inject_last_result=True, ), ], ) group = ActionGroup( name="outer_group", actions=[ Action(name="starter", action=lambda: 10), inner_chain, ], ) result = await group() result_dict = dict(result) assert result_dict["starter"] == 10 assert result_dict["inner"] == 4 @pytest.mark.asyncio async def test_mixed_sync_async_actions(): async def async_action(last_result): return last_result + 5 def sync_action(last_result): return last_result * 2 chain = ChainedAction( name="mixed_chain", actions=[ Action(name="start", action=lambda: 1), Action(name="double", action=sync_action, inject_last_result=True), Action(name="plus_five", action=async_action, inject_last_result=True), ], ) result = await chain() assert result == 7