Files
falyx/tests/test_parsers/test_resolve_args.py
Roland Thomas efe3f5fd99 feat(core): clone commands and actions when binding runtimes
Add clone support across Action types and Command so commands can be safely
registered or runner-bound without mutating the original instances.

- clone BaseAction implementations across simple, composite, IO, prompt, file,
  HTTP, process, and signal actions
- bind cloned commands in Falyx.add_command_from_command() and CommandRunner
- preserve local never_prompt settings when cloning actions
- rename shared runtime state from options to options_manager for consistency
- seed root and execution option namespaces consistently
- apply scoped root and namespace option overrides during routing and dispatch
- improve namespace completion by delegating option suggestions to FalyxParser
- enrich missing-value errors and error hints
2026-06-07 13:04:35 -04:00

243 lines
7.0 KiB
Python

import pytest
from falyx.command import Command
from falyx.exceptions import CommandArgumentError, NotAFalyxError
from falyx.execution_option import ExecutionOption
@pytest.mark.asyncio
async def test_resolve_args_separates_business_and_execution_options():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["summary", "retry"],
)
command.arg_parser.add_argument("--foo", type=int, help="A business argument.")
args, kwargs, execution_args = await command.resolve_args(
["--foo", "42", "--summary", "--retries", "3"]
)
assert args == ()
assert kwargs == {"foo": 42}
assert execution_args == {
"summary": True,
"retries": 3,
"retry_delay": 0.0,
"retry_backoff": 0.0,
}
args, kwargs, execution_args = await command.arg_parser.parse_args_split(
["--foo", "42", "--summary", "--retries", "3"]
)
assert args == ()
assert kwargs == {"foo": 42}
assert execution_args == {
"summary": True,
"retries": 3,
"retry_delay": 0.0,
"retry_backoff": 0.0,
}
@pytest.mark.asyncio
async def test_parse_args_split_with_no_execution_options_returns_empty_execution_args():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
)
command.arg_parser.add_argument("--foo", type=int, help="A business argument.")
args, kwargs, execution_args = await command.arg_parser.parse_args_split(
["--foo", "42"]
)
assert args == ()
assert kwargs == {"foo": 42}
assert execution_args == {}
@pytest.mark.asyncio
async def test_resolve_args_raises_on_conflicting_execution_option():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["summary"],
)
with pytest.raises(
CommandArgumentError, match="destination 'summary' is already defined"
):
command.arg_parser.add_argument(
"--summary", action="store_true", help="A conflicting argument."
)
with pytest.raises(
CommandArgumentError,
match="destination 'summary' is already registered as an execution argument",
):
command.arg_parser._register_execution_dest("summary")
@pytest.mark.asyncio
async def test_resolve_args_mix_of_business_and_execution_options():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["retry"],
)
command.arg_parser.add_argument("--summary", type=str, help="A business argument.")
args, kwargs, execution_args = await command.resolve_args(
["--summary", "test", "--retries", "5", "--retry-delay", "2"]
)
assert args == ()
assert kwargs == {"summary": "test"}
assert execution_args == {"retries": 5, "retry_delay": 2.0, "retry_backoff": 0.0}
@pytest.mark.asyncio
async def test_resolve_args_with_no_arguments():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["summary"],
)
args, kwargs, execution_args = await command.resolve_args([])
assert args == ()
assert kwargs == {}
assert execution_args == {"summary": False}
@pytest.mark.asyncio
async def test_resolve_args_with_confirmation_options():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["confirm"],
)
args, kwargs, execution_args = await command.resolve_args(["--confirm"])
assert args == ()
assert kwargs == {}
assert execution_args == {"force_confirm": True, "skip_confirm": False}
args, kwargs, execution_args = await command.resolve_args(["--skip-confirm"])
assert args == ()
assert kwargs == {}
assert execution_args == {"force_confirm": False, "skip_confirm": True}
@pytest.mark.asyncio
async def test_resolve_args_with_all_execution_options():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["summary", "retry", "confirm"],
)
args, kwargs, execution_args = await command.resolve_args(
["--summary", "--retries", "3", "--confirm"]
)
assert args == ()
assert kwargs == {}
assert execution_args == {
"summary": True,
"retries": 3,
"retry_delay": 0.0,
"retry_backoff": 0.0,
"force_confirm": True,
"skip_confirm": False,
}
@pytest.mark.asyncio
async def test_resolve_args_with_raw_string_input():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["summary"],
)
command.arg_parser.add_argument("--foo", type=int, help="A business argument.")
args, kwargs, execution_args = await command.resolve_args("--foo 42 --summary")
assert args == ()
assert kwargs == {"foo": 42}
assert execution_args == {"summary": True}
@pytest.mark.asyncio
async def test_resolve_args_with_no_arg_parser():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["summary"],
)
command.arg_parser = None
with pytest.raises(
NotAFalyxError,
match="Command has no parser configured. Provide a custom_parser or CommandArgumentParser.",
):
await command.resolve_args("--summary")
@pytest.mark.asyncio
async def test_resolve_args_with_custom_parser():
def parse_args_split(arg_list):
return (arg_list,), {}, {"custom_execution_arg": True}
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["summary"],
)
command.custom_parser = parse_args_split
args, kwargs, execution_args = await command.resolve_args("--summary")
assert args == (["--summary"],)
assert kwargs == {}
assert execution_args == {"custom_execution_arg": True}
# TODO: is this the right behavior? Should we expect the custom parser to handle non string inputs as well? Does this actually happen?
args, kwargs, execution_args = await command.resolve_args(2235235)
assert args == (2235235,)
assert kwargs == {}
assert execution_args == {"custom_execution_arg": True}
with pytest.raises(CommandArgumentError, match="Failed to parse arguments:"):
args, kwargs, execution_args = await command.resolve_args("unbalanced 'quotes")
@pytest.mark.asyncio
async def test_resolve_args_str_unbalanced_quotes():
command = Command.build(
key="T",
description="Test Command",
action=lambda: None,
execution_options=["summary"],
)
command.arg_parser.add_argument("--foo", type=str, help="A business argument.")
with pytest.raises(CommandArgumentError, match="Failed to parse arguments:"):
await command.resolve_args("--foo 'unbalanced quotes")