From c80b281dfc5b35ad807753072a1fadb53995f7f0 Mon Sep 17 00:00:00 2001 From: Tadej Krevh Date: Tue, 17 Dec 2024 01:59:27 +0100 Subject: [PATCH 1/3] Fixed a bug, if entered agent name was empty. Improved general text inputs with validations. Simplified code. --- agentstack/cli/cli.py | 139 +++++++++++++++++++++++------------------- agentstack/utils.py | 16 ++++- 2 files changed, 91 insertions(+), 64 deletions(-) diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index 0c085d5d..df2ee706 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -1,8 +1,8 @@ from typing import Optional -import os, sys +import os +import sys import time from datetime import datetime -from pathlib import Path import json import shutil @@ -26,8 +26,9 @@ from agentstack import inputs from agentstack.agents import get_all_agents from agentstack.tasks import get_all_tasks -from agentstack.utils import open_json_file, term_color, is_snake_case, get_framework +from agentstack.utils import open_json_file, term_color, is_snake_case, get_framework, validator_not_empty from agentstack.proj_templates import TemplateConfig +from agentstack.exceptions import ValidationError PREFERRED_MODELS = [ @@ -184,6 +185,75 @@ def ask_framework() -> str: return framework +def get_validated_input( + message: str, + validate_func=None, + min_length: int = 0, + snake_case: bool = False, +) -> str: + """Helper function to get validated input from user. + + Args: + message: The prompt message to display + validate_func: Optional custom validation function + min_length: Minimum length requirement (0 for no requirement) + snake_case: Whether to enforce snake_case naming + """ + while True: + try: + value = inquirer.text( + message=message, + validate=validate_func or validator_not_empty(min_length) if min_length else None, + ) + if snake_case and not is_snake_case(value): + raise ValidationError("Input must be in snake_case") + return value + except ValidationError as e: + print(term_color(f"Error: {str(e)}", 'red')) + + +def ask_agent_details(): + agent = {} + + agent['name'] = get_validated_input( + "What's the name of this agent? (snake_case)", min_length=3, snake_case=True + ) + + agent['role'] = get_validated_input("What role does this agent have?", min_length=3) + + agent['goal'] = get_validated_input("What is the goal of the agent?", min_length=10) + + agent['backstory'] = get_validated_input("Give your agent a backstory", min_length=10) + + agent['model'] = inquirer.list_input( + message="What LLM should this agent use?", choices=PREFERRED_MODELS, default=PREFERRED_MODELS[0] + ) + + return agent + + +def ask_task_details(agents: list[dict]) -> dict: + task = {} + + task['name'] = get_validated_input( + "What's the name of this task? (snake_case)", min_length=3, snake_case=True + ) + + task['description'] = get_validated_input("Describe the task in more detail", min_length=10) + + task['expected_output'] = get_validated_input( + "What do you expect the result to look like? (ex: A 5 bullet point summary of the email)", + min_length=10, + ) + + task['agent'] = inquirer.list_input( + message="Which agent should be assigned this task?", + choices=[a['name'] for a in agents], + ) + + return task + + def ask_design() -> dict: use_wizard = inquirer.confirm( message="Would you like to use the CLI wizard to set up agents and tasks?", @@ -208,39 +278,10 @@ def ask_design() -> dict: while make_agent: print('---') print(f"Agent #{len(agents)+1}") - - agent_incomplete = True agent = None - while agent_incomplete: - agent = inquirer.prompt( - [ - inquirer.Text("name", message="What's the name of this agent? (snake_case)"), - inquirer.Text("role", message="What role does this agent have?"), - inquirer.Text("goal", message="What is the goal of the agent?"), - inquirer.Text("backstory", message="Give your agent a backstory"), - # TODO: make a list - #2 - inquirer.Text( - 'model', - message="What LLM should this agent use? (any LiteLLM provider)", - default="openai/gpt-4", - ), - # inquirer.List("model", message="What LLM should this agent use? (any LiteLLM provider)", choices=[ - # 'mixtral_llm', - # 'mixtral_llm', - # ]), - ] - ) - - if not agent['name'] or agent['name'] == '': - print(term_color("Error: Agent name is required - Try again", 'red')) - agent_incomplete = True - elif not is_snake_case(agent['name']): - print(term_color("Error: Agent name must be snake case - Try again", 'red')) - else: - agent_incomplete = False - - make_agent = inquirer.confirm(message="Create another agent?") + agent = ask_agent_details() agents.append(agent) + make_agent = inquirer.confirm(message="Create another agent?") print('') for x in range(3): @@ -257,35 +298,9 @@ def ask_design() -> dict: while make_task: print('---') print(f"Task #{len(tasks) + 1}") - - task_incomplete = True - task = None - while task_incomplete: - task = inquirer.prompt( - [ - inquirer.Text("name", message="What's the name of this task? (snake_case)"), - inquirer.Text("description", message="Describe the task in more detail"), - inquirer.Text( - "expected_output", - message="What do you expect the result to look like? (ex: A 5 bullet point summary of the email)", - ), - inquirer.List( - "agent", - message="Which agent should be assigned this task?", - choices=[a['name'] for a in agents], # type: ignore - ), - ] - ) - - if not task['name'] or task['name'] == '': - print(term_color("Error: Task name is required - Try again", 'red')) - elif not is_snake_case(task['name']): - print(term_color("Error: Task name must be snake case - Try again", 'red')) - else: - task_incomplete = False - - make_task = inquirer.confirm(message="Create another task?") + task = ask_task_details(agents) tasks.append(task) + make_task = inquirer.confirm(message="Create another task?") print('') for x in range(3): diff --git a/agentstack/utils.py b/agentstack/utils.py index e1564dc2..a3c57988 100644 --- a/agentstack/utils.py +++ b/agentstack/utils.py @@ -1,5 +1,5 @@ -from typing import Optional, Union -import os, sys +import os +import sys import json from ruamel.yaml import YAML import re @@ -7,6 +7,7 @@ from pathlib import Path import importlib.resources from agentstack import conf +from inquirer import errors as inquirer_errors def get_version(package: str = 'agentstack'): @@ -108,3 +109,14 @@ def term_color(text: str, color: str) -> str: def is_snake_case(string: str): return bool(re.match('^[a-z0-9_]+$', string)) + + +def validator_not_empty(min_length=1): + def validator(_, answer): + if len(answer) < min_length: + raise inquirer_errors.ValidationError( + '', reason=f"This field must be at least {min_length} characters long." + ) + return True + + return validator From 1be42665bb4e03cd217aa0273460c87a0c91091e Mon Sep 17 00:00:00 2001 From: Tadej Krevh Date: Tue, 17 Dec 2024 21:35:28 +0100 Subject: [PATCH 2/3] Added tests for new functions. Refactored subprocess.run to be cross-platform compatible. Fixed failing tests on win32. --- tests/cli_test_utils.py | 36 ++++++++++++++++++++++++++++ tests/test_cli_init.py | 15 ++++++------ tests/test_cli_loads.py | 16 +++++-------- tests/test_cli_templates.py | 15 +++++------- tests/test_cli_tools.py | 47 +++++++++++++++++++++++++++++-------- tests/test_utils.py | 21 ++++++++++++++++- 6 files changed, 112 insertions(+), 38 deletions(-) create mode 100644 tests/cli_test_utils.py diff --git a/tests/cli_test_utils.py b/tests/cli_test_utils.py new file mode 100644 index 00000000..fea013cc --- /dev/null +++ b/tests/cli_test_utils.py @@ -0,0 +1,36 @@ +import os, sys +import subprocess + +def run_cli(cli_entry, *args): + """Helper method to run the CLI with arguments. Cross-platform.""" + try: + # Use shell=True on Windows to handle path issues + if sys.platform == 'win32': + # Add PYTHONIOENCODING to the environment + env = os.environ.copy() + env['PYTHONIOENCODING'] = 'utf-8' + result = subprocess.run( + " ".join(str(arg) for arg in cli_entry + list(args)), + capture_output=True, + text=True, + shell=True, + env=env, + encoding='utf-8' + ) + else: + result = subprocess.run( + [*cli_entry, *args], + capture_output=True, + text=True, + encoding='utf-8' + ) + + if result.returncode != 0: + print(f"Command failed with code {result.returncode}") + print(f"STDOUT: {result.stdout}") + print(f"STDERR: {result.stderr}") + + return result + except Exception as e: + print(f"Exception running command: {e}") + raise \ No newline at end of file diff --git a/tests/test_cli_init.py b/tests/test_cli_init.py index b07d7a3f..218f7821 100644 --- a/tests/test_cli_init.py +++ b/tests/test_cli_init.py @@ -1,9 +1,9 @@ -import subprocess import os, sys import unittest from parameterized import parameterized from pathlib import Path import shutil +from cli_test_utils import run_cli BASE_PATH = Path(__file__).parent CLI_ENTRY = [ @@ -16,18 +16,17 @@ class CLIInitTest(unittest.TestCase): def setUp(self): self.project_dir = Path(BASE_PATH / 'tmp/cli_init') - os.makedirs(self.project_dir) + os.chdir(BASE_PATH) # Change to parent directory first + os.makedirs(self.project_dir, exist_ok=True) os.chdir(self.project_dir) + # Force UTF-8 encoding for the test environment + os.environ['PYTHONIOENCODING'] = 'utf-8' def tearDown(self): - shutil.rmtree(self.project_dir) - - def _run_cli(self, *args): - """Helper method to run the CLI with arguments.""" - return subprocess.run([*CLI_ENTRY, *args], capture_output=True, text=True) + shutil.rmtree(self.project_dir, ignore_errors=True) def test_init_command(self): """Test the 'init' command to create a project directory.""" - result = self._run_cli('init', 'test_project') + result = run_cli(CLI_ENTRY, 'init', 'test_project') self.assertEqual(result.returncode, 0) self.assertTrue((self.project_dir / 'test_project').exists()) diff --git a/tests/test_cli_loads.py b/tests/test_cli_loads.py index 6ac8fcad..2a50ee0f 100644 --- a/tests/test_cli_loads.py +++ b/tests/test_cli_loads.py @@ -3,6 +3,7 @@ import unittest from pathlib import Path import shutil +from cli_test_utils import run_cli BASE_PATH = Path(__file__).parent @@ -14,14 +15,9 @@ class TestAgentStackCLI(unittest.TestCase): "agentstack.main", ] - def run_cli(self, *args): - """Helper method to run the CLI with arguments.""" - result = subprocess.run([*self.CLI_ENTRY, *args], capture_output=True, text=True) - return result - def test_version(self): """Test the --version command.""" - result = self.run_cli("--version") + result = run_cli(self.CLI_ENTRY, "--version") print(result.stdout) print(result.stderr) print(result.returncode) @@ -30,7 +26,7 @@ def test_version(self): def test_invalid_command(self): """Test an invalid command gracefully exits.""" - result = self.run_cli("invalid_command") + result = run_cli(self.CLI_ENTRY, "invalid_command") self.assertNotEqual(result.returncode, 0) self.assertIn("usage:", result.stderr) @@ -38,7 +34,7 @@ def test_run_command_invalid_project(self): """Test the 'run' command on an invalid project.""" test_dir = Path(BASE_PATH / 'tmp/test_project') if test_dir.exists(): - shutil.rmtree(test_dir) + shutil.rmtree(test_dir, ignore_errors=True) os.makedirs(test_dir) # Write a basic agentstack.json file @@ -46,11 +42,11 @@ def test_run_command_invalid_project(self): f.write(open(BASE_PATH / 'fixtures/agentstack.json', 'r').read()) os.chdir(test_dir) - result = self.run_cli('run') + result = run_cli(self.CLI_ENTRY, 'run') self.assertNotEqual(result.returncode, 0) self.assertIn("Project validation failed", result.stdout) - shutil.rmtree(test_dir) + shutil.rmtree(test_dir, ignore_errors=True) if __name__ == "__main__": diff --git a/tests/test_cli_templates.py b/tests/test_cli_templates.py index 285b91c1..a791bb9b 100644 --- a/tests/test_cli_templates.py +++ b/tests/test_cli_templates.py @@ -5,6 +5,7 @@ from pathlib import Path import shutil from agentstack.proj_templates import get_all_template_names +from cli_test_utils import run_cli BASE_PATH = Path(__file__).parent CLI_ENTRY = [ @@ -17,20 +18,16 @@ class CLITemplatesTest(unittest.TestCase): def setUp(self): self.project_dir = Path(BASE_PATH / 'tmp/cli_templates') - os.makedirs(self.project_dir) + os.makedirs(self.project_dir, exist_ok=True) os.chdir(self.project_dir) def tearDown(self): - shutil.rmtree(self.project_dir) - - def _run_cli(self, *args): - """Helper method to run the CLI with arguments.""" - return subprocess.run([*CLI_ENTRY, *args], capture_output=True, text=True) + shutil.rmtree(self.project_dir, ignore_errors=True) @parameterized.expand([(x,) for x in get_all_template_names()]) def test_init_command_for_template(self, template_name): """Test the 'init' command to create a project directory with a template.""" - result = self._run_cli('init', 'test_project', '--template', template_name) + result = run_cli(CLI_ENTRY, 'init', 'test_project', '--template', template_name) self.assertEqual(result.returncode, 0) self.assertTrue((self.project_dir / 'test_project').exists()) @@ -39,7 +36,7 @@ def test_export_template_v1(self): result = self._run_cli('init', f"test_project") self.assertEqual(result.returncode, 0) os.chdir(self.project_dir / f"test_project") - result = self._run_cli('generate', 'agent', 'test_agent', '--llm', 'opeenai/gpt-4o') + result = self._run_cli('generate', 'agent', 'test_agent', '--llm', 'openai/gpt-4o') self.assertEqual(result.returncode, 0) result = self._run_cli('generate', 'task', 'test_task', '--agent', 'test_agent') self.assertEqual(result.returncode, 0) @@ -67,7 +64,7 @@ def test_export_template_v1(self): "role": "Add your role here", "goal": "Add your goal here", "backstory": "Add your backstory here", - "model": "opeenai/gpt-4o" + "model": "openai/gpt-4o" } ], "tasks": [ diff --git a/tests/test_cli_tools.py b/tests/test_cli_tools.py index f5048b72..92ab425f 100644 --- a/tests/test_cli_tools.py +++ b/tests/test_cli_tools.py @@ -5,6 +5,10 @@ from pathlib import Path import shutil from agentstack.tools import get_all_tool_names +from cli_test_utils import run_cli +from agentstack.utils import validator_not_empty +from agentstack.cli.cli import get_validated_input + BASE_PATH = Path(__file__).parent CLI_ENTRY = [ @@ -18,29 +22,52 @@ class CLIToolsTest(unittest.TestCase): def setUp(self): self.project_dir = Path(BASE_PATH / 'tmp/cli_tools') - os.makedirs(self.project_dir) + os.makedirs(self.project_dir, exist_ok=True) os.chdir(self.project_dir) def tearDown(self): - shutil.rmtree(self.project_dir) - - def _run_cli(self, *args): - """Helper method to run the CLI with arguments.""" - return subprocess.run([*CLI_ENTRY, *args], capture_output=True, text=True) + shutil.rmtree(self.project_dir, ignore_errors=True) @parameterized.expand([(x,) for x in get_all_tool_names()]) @unittest.skip("Dependency resolution issue") def test_add_tool(self, tool_name): """Test the adding every tool to a project.""" - result = self._run_cli('init', f"{tool_name}_project") + result = run_cli(CLI_ENTRY, 'init', f"{tool_name}_project") self.assertEqual(result.returncode, 0) os.chdir(self.project_dir / f"{tool_name}_project") - result = self._run_cli('generate', 'agent', 'test_agent', '--llm', 'opeenai/gpt-4o') + result = run_cli(CLI_ENTRY, 'generate', 'agent', 'test_agent', '--llm', 'opeenai/gpt-4o') self.assertEqual(result.returncode, 0) - result = self._run_cli('generate', 'task', 'test_task') + result = run_cli(CLI_ENTRY, 'generate', 'task', 'test_task') self.assertEqual(result.returncode, 0) - result = self._run_cli('tools', 'add', tool_name) + result = run_cli(CLI_ENTRY, 'tools', 'add', tool_name) print(result.stdout) self.assertEqual(result.returncode, 0) self.assertTrue(self.project_dir.exists()) + + def test_get_validated_input(self): + """Test the get_validated_input function with various validation scenarios""" + from agentstack.cli.cli import get_validated_input + from unittest.mock import patch + from inquirer.errors import ValidationError + from agentstack.utils import validator_not_empty + + # Test basic input + with patch('inquirer.text', return_value='test_input'): + result = get_validated_input("Test message") + self.assertEqual(result, 'test_input') + + # Test min length validation - valid input + with patch('inquirer.text', return_value='abc'): + result = get_validated_input("Test message", min_length=3) + self.assertEqual(result, 'abc') + + # Test min length validation - invalid input should raise ValidationError + validator = validator_not_empty(3) + with self.assertRaises(ValidationError): + validator(None, 'ab') + + # Test snake_case validation + with patch('inquirer.text', return_value='test_case'): + result = get_validated_input("Test message", snake_case=True) + self.assertEqual(result, 'test_case') \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py index 720965d2..6b210ab7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,7 @@ import unittest -from agentstack.utils import clean_input, is_snake_case +from agentstack.utils import clean_input, is_snake_case, validator_not_empty +from inquirer import errors as inquirer_errors class TestUtils(unittest.TestCase): @@ -18,3 +19,21 @@ def test_is_snake_case(self): assert not is_snake_case("Hello-World") assert not is_snake_case("hello-world") assert not is_snake_case("hello world") + + def test_validator_not_empty(self): + validator = validator_not_empty(min_length=1) + + # Valid input should return True + self.assertTrue(validator(None, "test")) + self.assertTrue(validator(None, "a")) + + # Empty input should raise ValidationError + with self.assertRaises(inquirer_errors.ValidationError): + validator(None, "") + + # Test with larger min_length + validator = validator_not_empty(min_length=3) + self.assertTrue(validator(None, "test")) + with self.assertRaises(inquirer_errors.ValidationError): + validator(None, "ab") + From 1b18fb953cfc20a7a0e2cd2075bd50dd44e85fa4 Mon Sep 17 00:00:00 2001 From: Tadej Krevh Date: Thu, 19 Dec 2024 10:09:03 +0100 Subject: [PATCH 3/3] Moved CLI_ENTRY out of individual test files to cli_test_utils as it was the same across all tests --- tests/cli_test_utils.py | 12 +++++++++--- tests/test_cli_init.py | 8 +------- tests/test_cli_loads.py | 11 +++-------- tests/test_cli_templates.py | 8 +------- tests/test_cli_tools.py | 21 +++++++-------------- 5 files changed, 21 insertions(+), 39 deletions(-) diff --git a/tests/cli_test_utils.py b/tests/cli_test_utils.py index fea013cc..43277d4c 100644 --- a/tests/cli_test_utils.py +++ b/tests/cli_test_utils.py @@ -1,7 +1,13 @@ import os, sys import subprocess -def run_cli(cli_entry, *args): +CLI_ENTRY = [ + sys.executable, + "-m", + "agentstack.main", +] + +def run_cli(*args): """Helper method to run the CLI with arguments. Cross-platform.""" try: # Use shell=True on Windows to handle path issues @@ -10,7 +16,7 @@ def run_cli(cli_entry, *args): env = os.environ.copy() env['PYTHONIOENCODING'] = 'utf-8' result = subprocess.run( - " ".join(str(arg) for arg in cli_entry + list(args)), + " ".join(str(arg) for arg in CLI_ENTRY + list(args)), capture_output=True, text=True, shell=True, @@ -19,7 +25,7 @@ def run_cli(cli_entry, *args): ) else: result = subprocess.run( - [*cli_entry, *args], + [*CLI_ENTRY, *args], capture_output=True, text=True, encoding='utf-8' diff --git a/tests/test_cli_init.py b/tests/test_cli_init.py index 218f7821..02267d35 100644 --- a/tests/test_cli_init.py +++ b/tests/test_cli_init.py @@ -6,12 +6,6 @@ from cli_test_utils import run_cli BASE_PATH = Path(__file__).parent -CLI_ENTRY = [ - sys.executable, - "-m", - "agentstack.main", -] - class CLIInitTest(unittest.TestCase): def setUp(self): @@ -27,6 +21,6 @@ def tearDown(self): def test_init_command(self): """Test the 'init' command to create a project directory.""" - result = run_cli(CLI_ENTRY, 'init', 'test_project') + result = run_cli('init', 'test_project') self.assertEqual(result.returncode, 0) self.assertTrue((self.project_dir / 'test_project').exists()) diff --git a/tests/test_cli_loads.py b/tests/test_cli_loads.py index 2a50ee0f..074e6caf 100644 --- a/tests/test_cli_loads.py +++ b/tests/test_cli_loads.py @@ -9,15 +9,10 @@ class TestAgentStackCLI(unittest.TestCase): - CLI_ENTRY = [ - sys.executable, - "-m", - "agentstack.main", - ] def test_version(self): """Test the --version command.""" - result = run_cli(self.CLI_ENTRY, "--version") + result = run_cli("--version") print(result.stdout) print(result.stderr) print(result.returncode) @@ -26,7 +21,7 @@ def test_version(self): def test_invalid_command(self): """Test an invalid command gracefully exits.""" - result = run_cli(self.CLI_ENTRY, "invalid_command") + result = run_cli("invalid_command") self.assertNotEqual(result.returncode, 0) self.assertIn("usage:", result.stderr) @@ -42,7 +37,7 @@ def test_run_command_invalid_project(self): f.write(open(BASE_PATH / 'fixtures/agentstack.json', 'r').read()) os.chdir(test_dir) - result = run_cli(self.CLI_ENTRY, 'run') + result = run_cli('run') self.assertNotEqual(result.returncode, 0) self.assertIn("Project validation failed", result.stdout) diff --git a/tests/test_cli_templates.py b/tests/test_cli_templates.py index a791bb9b..f9bba704 100644 --- a/tests/test_cli_templates.py +++ b/tests/test_cli_templates.py @@ -8,12 +8,6 @@ from cli_test_utils import run_cli BASE_PATH = Path(__file__).parent -CLI_ENTRY = [ - sys.executable, - "-m", - "agentstack.main", -] - class CLITemplatesTest(unittest.TestCase): def setUp(self): @@ -27,7 +21,7 @@ def tearDown(self): @parameterized.expand([(x,) for x in get_all_template_names()]) def test_init_command_for_template(self, template_name): """Test the 'init' command to create a project directory with a template.""" - result = run_cli(CLI_ENTRY, 'init', 'test_project', '--template', template_name) + result = run_cli('init', 'test_project', '--template', template_name) self.assertEqual(result.returncode, 0) self.assertTrue((self.project_dir / 'test_project').exists()) diff --git a/tests/test_cli_tools.py b/tests/test_cli_tools.py index 92ab425f..9443639c 100644 --- a/tests/test_cli_tools.py +++ b/tests/test_cli_tools.py @@ -8,15 +8,12 @@ from cli_test_utils import run_cli from agentstack.utils import validator_not_empty from agentstack.cli.cli import get_validated_input +from unittest.mock import patch +from inquirer.errors import ValidationError +from agentstack.utils import validator_not_empty BASE_PATH = Path(__file__).parent -CLI_ENTRY = [ - sys.executable, - "-m", - "agentstack.main", -] - # TODO parameterized framework class CLIToolsTest(unittest.TestCase): @@ -32,25 +29,21 @@ def tearDown(self): @unittest.skip("Dependency resolution issue") def test_add_tool(self, tool_name): """Test the adding every tool to a project.""" - result = run_cli(CLI_ENTRY, 'init', f"{tool_name}_project") + result = run_cli('init', f"{tool_name}_project") self.assertEqual(result.returncode, 0) os.chdir(self.project_dir / f"{tool_name}_project") - result = run_cli(CLI_ENTRY, 'generate', 'agent', 'test_agent', '--llm', 'opeenai/gpt-4o') + result = run_cli('generate', 'agent', 'test_agent', '--llm', 'opeenai/gpt-4o') self.assertEqual(result.returncode, 0) - result = run_cli(CLI_ENTRY, 'generate', 'task', 'test_task') + result = run_cli('generate', 'task', 'test_task') self.assertEqual(result.returncode, 0) - result = run_cli(CLI_ENTRY, 'tools', 'add', tool_name) + result = run_cli('tools', 'add', tool_name) print(result.stdout) self.assertEqual(result.returncode, 0) self.assertTrue(self.project_dir.exists()) def test_get_validated_input(self): """Test the get_validated_input function with various validation scenarios""" - from agentstack.cli.cli import get_validated_input - from unittest.mock import patch - from inquirer.errors import ValidationError - from agentstack.utils import validator_not_empty # Test basic input with patch('inquirer.text', return_value='test_input'):