feat(core): advance options/state handling and workflow execution integration

- extend OptionsManager to support multi-namespace option resolution and toggling
- integrate OptionsManager more deeply across Action, ChainedAction, and ActionGroup
- propagate shared runtime configuration through execution layers
- refine action composition model (sequential + parallel execution semantics)
- improve lifecycle consistency across BaseAction, Action, ChainedAction, and ActionGroup
- begin aligning execution flow with centralized context and options handling

wip: routing and root option parsing behavior still in progress
This commit is contained in:
2026-05-10 13:48:06 -04:00
parent cce92cca09
commit 8db7a9e6dc
47 changed files with 2886 additions and 1089 deletions

View File

@@ -0,0 +1,46 @@
import pytest
from falyx import Falyx
from falyx.action import Action
@pytest.mark.asyncio
async def test_execute_command():
"""Test if Falyx can run in run key mode."""
falyx = Falyx("Run Key Test")
# Add a simple command
falyx.add_command(
key="T",
description="Test Command",
action=lambda: "Hello, World!",
)
# Run the CLI
result = await falyx.execute_command("T")
assert result == "Hello, World!"
@pytest.mark.asyncio
async def test_execute_command_recover():
"""Test if Falyx can recover from a failure in run key mode."""
falyx = Falyx("Run Key Recovery Test")
state = {"count": 0}
async def flaky():
if not state["count"]:
state["count"] += 1
raise RuntimeError("Random failure!")
return "ok"
# Add a command that raises an exception
falyx.add_command(
key="E",
description="Error Command",
action=Action("flaky", flaky),
retry=True,
)
result = await falyx.execute_command("E")
assert result == "ok"

View File

@@ -2,7 +2,7 @@ import pytest
from rich.text import Text
from falyx import Falyx
from falyx.console import console
from falyx.exceptions import CommandArgumentError
@pytest.mark.asyncio
@@ -82,17 +82,14 @@ async def test_help_command_by_tag(capsys):
@pytest.mark.asyncio
async def test_help_command_empty_tags(capsys):
async def test_help_command_bad_argument(capsys):
flx = Falyx()
async def untagged_command(falyx: Falyx):
pass
flx.add_command(
"U", "Untagged Command", untagged_command, help_text="This command has no tags."
)
await flx.execute_command("H nonexistent_tag")
captured = capsys.readouterr()
text = Text.from_ansi(captured.out)
assert "Unexpected positional argument: nonexistent_tag" in text.plain
flx.add_command("U", "Untagged Command", untagged_command)
with pytest.raises(
CommandArgumentError, match="Unexpected positional argument: nonexistent_tag"
):
await flx.execute_command("H nonexistent_tag")

View File

View File

@@ -1,8 +1,72 @@
import asyncio
import sys
import pytest
from rich.text import Text
from falyx import Falyx
from falyx.console import console as falyx_console
from falyx.exceptions import FalyxError
from falyx.parser import ParseResult
from falyx.signals import BackSignal, CancelSignal, FlowSignal, HelpSignal, QuitSignal
async def throw_error_action(error: str):
if error == "QuitSignal":
raise QuitSignal("Quit signal triggered.")
elif error == "BackSignal":
raise BackSignal("Back signal triggered.")
elif error == "CancelSignal":
raise CancelSignal("Cancel signal triggered.")
elif error == "ValueError":
raise ValueError("This is a ValueError.")
elif error == "HelpSignal":
raise HelpSignal("Help signal triggered.")
elif error == "FalyxError":
raise FalyxError("This is a FalyxError.")
elif error == "FlowSignal":
raise FlowSignal("Flow signal triggered.")
else:
raise asyncio.CancelledError("An error occurred in the action.")
@pytest.fixture
def flx() -> Falyx:
sys.argv = ["falyx", "T"]
flx = Falyx()
flx.add_command(
"T",
"Test",
action=lambda: "hello",
)
flx.add_tldr_example(
entry_key="T",
usage="",
description="This is a TLDR example for the T command.",
)
return flx
@pytest.fixture
def flx_with_submenu() -> Falyx:
flx = Falyx()
submenu = Falyx("Submenu")
submenu.add_command(
"T",
"Test",
action=lambda: "hello from submenu",
)
submenu.add_tldr_example(
entry_key="T",
usage="",
description="This is a TLDR example for the T command in the submenu.",
)
flx.add_submenu(
"S",
"Submenu",
submenu=submenu,
)
return flx
@pytest.mark.asyncio
@@ -14,3 +78,178 @@ async def test_run_basic(capsys):
captured = capsys.readouterr()
assert "Show this help menu." in captured.out
@pytest.mark.asyncio
async def test_run_default_to_menu(flx):
sys.argv = ["falyx", "T"]
flx.default_to_menu = False
with pytest.raises(SystemExit):
await flx.run()
await flx.run(always_start_menu=True)
@pytest.mark.asyncio
async def test_run_default_to_menu_help(flx):
sys.argv = ["falyx"]
flx.default_to_menu = False
with pytest.raises(SystemExit, match="0"):
with falyx_console.capture() as capture:
await flx.run()
captured = Text.from_ansi(capture.get()).plain
assert "Show this help menu." in captured
@pytest.mark.asyncio
async def test_run_debug_hooks(flx):
sys.argv = ["falyx", "--debug-hooks", "T"]
assert flx.options.get("debug_hooks") is False
with pytest.raises(SystemExit):
await flx.run()
assert flx.options.get("debug_hooks") is True
@pytest.mark.asyncio
async def test_run_never_prompt(flx):
sys.argv = ["falyx", "--never-prompt", "T"]
assert flx.options.get("never_prompt") is False
with pytest.raises(SystemExit):
await flx.run()
falyx_console.print(flx.options.get_namespace_dict("default"))
assert flx.options.get("debug_hooks") is False
assert flx.options.get("never_prompt") is True
@pytest.mark.asyncio
async def test_run_bad_args(flx):
sys.argv = ["falyx", "T", "--unknown-arg"]
with pytest.raises(SystemExit, match="2"):
await flx.run()
@pytest.mark.asyncio
async def test_run_help(flx):
sys.argv = ["falyx", "T", "--help"]
with pytest.raises(SystemExit, match="0"):
await flx.run()
sys.argv = ["falyx", "--help"]
with pytest.raises(SystemExit, match="0"):
await flx.run()
sys.argv = ["falyx", "-h"]
with pytest.raises(SystemExit, match="0"):
await flx.run()
sys.argv = ["falyx", "--tldr"]
with pytest.raises(SystemExit, match="0"):
await flx.run()
sys.argv = ["falyx", "-T"]
with pytest.raises(SystemExit, match="0"):
await flx.run()
@pytest.mark.asyncio
async def test_run_entry_not_found(flx):
sys.argv = ["falyx", "UNKNOWN_COMMAND"]
with pytest.raises(SystemExit, match="2"):
await flx.run()
@pytest.mark.asyncio
async def test_run_test_exceptions(flx):
flx.add_command(
"E",
"Throw Error",
action=throw_error_action,
)
sys.argv = ["falyx", "E", "ValueError"]
with pytest.raises(SystemExit, match="1"):
await flx.run()
sys.argv = ["falyx", "E", "QuitSignal"]
with pytest.raises(SystemExit, match="130"):
await flx.run()
sys.argv = ["falyx", "E", "BackSignal"]
with pytest.raises(SystemExit, match="1"):
await flx.run()
sys.argv = ["falyx", "E", "CancelSignal"]
with pytest.raises(SystemExit, match="1"):
await flx.run()
sys.argv = ["falyx", "E", "HelpSignal"]
with pytest.raises(SystemExit, match="1"):
await flx.run()
sys.argv = ["falyx", "E", "FlowSignal"]
with pytest.raises(SystemExit, match="1"):
await flx.run()
sys.argv = ["falyx", "--verbose", "E", "FalyxError"]
with pytest.raises(SystemExit, match="1"):
await flx.run()
sys.argv = ["falyx", "E", "UnknownError"]
with pytest.raises(SystemExit, match="1"):
await flx.run()
@pytest.mark.asyncio
async def test_run_no_args(flx):
sys.argv = ["falyx"]
with pytest.raises(SystemExit, match="0"):
await flx.run()
@pytest.mark.asyncio
async def test_run_submenu(flx_with_submenu):
sys.argv = ["falyx", "S", "T"]
with pytest.raises(SystemExit, match="0"):
await flx_with_submenu.run()
@pytest.mark.asyncio
async def test_run_submenu_help(flx_with_submenu):
sys.argv = ["falyx", "S", "--help"]
with pytest.raises(SystemExit, match="0"):
await flx_with_submenu.run()
@pytest.mark.asyncio
async def test_run_submenu_tldr(flx_with_submenu):
sys.argv = ["falyx", "S", "--tldr"]
with pytest.raises(SystemExit, match="0"):
await flx_with_submenu.run()
@pytest.mark.asyncio
async def test_run_preview(flx):
sys.argv = ["falyx", "preview", "T"]
with pytest.raises(SystemExit, match="0"):
with falyx_console.capture() as capture:
await flx.run()
captured = Text.from_ansi(capture.get()).plain
assert "Command: 'T'" in captured
assert "Would call: <lambda>(args=(), kwargs={})" in captured