ara-cli 0.1.10.5__py3-none-any.whl → 0.1.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ara_cli/__init__.py +51 -6
- ara_cli/__main__.py +87 -75
- ara_cli/ara_command_action.py +189 -101
- ara_cli/ara_config.py +187 -128
- ara_cli/ara_subcommands/common.py +2 -2
- ara_cli/ara_subcommands/config.py +221 -0
- ara_cli/ara_subcommands/convert.py +107 -0
- ara_cli/ara_subcommands/fetch.py +41 -0
- ara_cli/ara_subcommands/fetch_agents.py +22 -0
- ara_cli/ara_subcommands/fetch_scripts.py +19 -0
- ara_cli/ara_subcommands/fetch_templates.py +15 -10
- ara_cli/ara_subcommands/list.py +97 -23
- ara_cli/ara_subcommands/prompt.py +266 -106
- ara_cli/artefact_autofix.py +117 -64
- ara_cli/artefact_converter.py +355 -0
- ara_cli/artefact_creator.py +41 -17
- ara_cli/artefact_lister.py +3 -3
- ara_cli/artefact_models/artefact_model.py +1 -1
- ara_cli/artefact_models/artefact_templates.py +0 -9
- ara_cli/artefact_models/feature_artefact_model.py +8 -8
- ara_cli/artefact_reader.py +62 -43
- ara_cli/artefact_scan.py +39 -17
- ara_cli/chat.py +300 -71
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_process_manager.py +155 -0
- ara_cli/chat_script_runner/__init__.py +0 -0
- ara_cli/chat_script_runner/script_completer.py +23 -0
- ara_cli/chat_script_runner/script_finder.py +41 -0
- ara_cli/chat_script_runner/script_lister.py +36 -0
- ara_cli/chat_script_runner/script_runner.py +36 -0
- ara_cli/chat_web_search/__init__.py +0 -0
- ara_cli/chat_web_search/web_search.py +263 -0
- ara_cli/children_contribution_updater.py +737 -0
- ara_cli/classifier.py +34 -0
- ara_cli/commands/agent_run_command.py +98 -0
- ara_cli/commands/fetch_agents_command.py +106 -0
- ara_cli/commands/fetch_scripts_command.py +43 -0
- ara_cli/commands/fetch_templates_command.py +39 -0
- ara_cli/commands/fetch_templates_commands.py +39 -0
- ara_cli/commands/list_agents_command.py +39 -0
- ara_cli/commands/load_command.py +4 -3
- ara_cli/commands/load_image_command.py +1 -1
- ara_cli/commands/read_command.py +23 -27
- ara_cli/completers.py +95 -35
- ara_cli/constants.py +2 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +26 -11
- ara_cli/file_loaders/document_reader.py +0 -178
- ara_cli/file_loaders/factories/__init__.py +0 -0
- ara_cli/file_loaders/factories/document_reader_factory.py +32 -0
- ara_cli/file_loaders/factories/file_loader_factory.py +27 -0
- ara_cli/file_loaders/file_loader.py +1 -30
- ara_cli/file_loaders/loaders/__init__.py +0 -0
- ara_cli/file_loaders/{document_file_loader.py → loaders/document_file_loader.py} +1 -1
- ara_cli/file_loaders/loaders/text_file_loader.py +47 -0
- ara_cli/file_loaders/readers/__init__.py +0 -0
- ara_cli/file_loaders/readers/docx_reader.py +49 -0
- ara_cli/file_loaders/readers/excel_reader.py +27 -0
- ara_cli/file_loaders/{markdown_reader.py → readers/markdown_reader.py} +1 -1
- ara_cli/file_loaders/readers/odt_reader.py +59 -0
- ara_cli/file_loaders/readers/pdf_reader.py +54 -0
- ara_cli/file_loaders/readers/pptx_reader.py +104 -0
- ara_cli/file_loaders/tools/__init__.py +0 -0
- ara_cli/llm_utils.py +58 -0
- ara_cli/output_suppressor.py +53 -0
- ara_cli/prompt_chat.py +20 -4
- ara_cli/prompt_extractor.py +47 -32
- ara_cli/prompt_handler.py +123 -17
- ara_cli/tag_extractor.py +8 -7
- ara_cli/template_loader.py +2 -1
- ara_cli/template_manager.py +52 -21
- ara_cli/templates/global-scripts/hello_global.py +1 -0
- ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
- ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
- ara_cli/version.py +1 -1
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/METADATA +49 -11
- ara_cli-0.1.14.0.dist-info/RECORD +253 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/WHEEL +1 -1
- tests/test_ara_command_action.py +31 -19
- tests/test_ara_config.py +177 -90
- tests/test_artefact_autofix.py +170 -97
- tests/test_artefact_autofix_integration.py +495 -0
- tests/test_artefact_converter.py +312 -0
- tests/test_artefact_extraction.py +564 -0
- tests/test_artefact_lister.py +11 -8
- tests/test_chat.py +166 -130
- tests/test_chat_givens_images.py +603 -0
- tests/test_chat_script_runner.py +454 -0
- tests/test_children_contribution_updater.py +98 -0
- tests/test_document_loader_office.py +267 -0
- tests/test_llm_utils.py +164 -0
- tests/test_prompt_chat.py +343 -0
- tests/test_prompt_extractor.py +683 -0
- tests/test_prompt_handler.py +416 -214
- tests/test_setup_default_chat_prompt_mode.py +198 -0
- tests/test_tag_extractor.py +95 -49
- tests/test_web_search.py +467 -0
- ara_cli/file_loaders/document_readers.py +0 -233
- ara_cli/file_loaders/file_loaders.py +0 -123
- ara_cli/file_loaders/text_file_loader.py +0 -187
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/pytest_unittest_prompt.blueprint.md +0 -32
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli-0.1.10.5.dist-info/RECORD +0 -194
- /ara_cli/file_loaders/{binary_file_loader.py → loaders/binary_file_loader.py} +0 -0
- /ara_cli/file_loaders/{image_processor.py → tools/image_processor.py} +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for chat_script_runner modules.
|
|
3
|
+
|
|
4
|
+
Provides full test coverage for:
|
|
5
|
+
- script_completer.py
|
|
6
|
+
- script_finder.py
|
|
7
|
+
- script_lister.py
|
|
8
|
+
- script_runner.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
from unittest.mock import patch, MagicMock
|
|
15
|
+
from ara_cli.chat_script_runner.script_completer import ScriptCompleter
|
|
16
|
+
from ara_cli.chat_script_runner.script_finder import ScriptFinder
|
|
17
|
+
from ara_cli.chat_script_runner.script_lister import ScriptLister
|
|
18
|
+
from ara_cli.chat_script_runner.script_runner import ScriptRunner
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# Tests for ScriptFinder
|
|
23
|
+
# =============================================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestScriptFinder:
|
|
27
|
+
"""Tests for ScriptFinder class."""
|
|
28
|
+
|
|
29
|
+
@patch("ara_cli.chat_script_runner.script_finder.ConfigManager.get_config")
|
|
30
|
+
def test_get_custom_scripts_dir(self, mock_get_config):
|
|
31
|
+
"""Returns custom scripts directory path."""
|
|
32
|
+
mock_config = MagicMock()
|
|
33
|
+
mock_config.local_prompt_templates_dir = "/path/to/templates"
|
|
34
|
+
mock_get_config.return_value = mock_config
|
|
35
|
+
|
|
36
|
+
finder = ScriptFinder()
|
|
37
|
+
result = finder.get_custom_scripts_dir()
|
|
38
|
+
|
|
39
|
+
assert result == os.path.join("/path/to/templates", "custom-scripts")
|
|
40
|
+
|
|
41
|
+
@patch("ara_cli.chat_script_runner.script_finder.ConfigManager.get_config")
|
|
42
|
+
def test_get_global_scripts_dir(self, mock_get_config):
|
|
43
|
+
"""Returns global scripts directory path."""
|
|
44
|
+
mock_config = MagicMock()
|
|
45
|
+
mock_config.local_prompt_templates_dir = "/path/to/templates"
|
|
46
|
+
mock_get_config.return_value = mock_config
|
|
47
|
+
|
|
48
|
+
finder = ScriptFinder()
|
|
49
|
+
result = finder.get_global_scripts_dir()
|
|
50
|
+
|
|
51
|
+
assert result == os.path.join("/path/to/templates", "global-scripts")
|
|
52
|
+
|
|
53
|
+
@patch("ara_cli.chat_script_runner.script_finder.ConfigManager.get_config")
|
|
54
|
+
@patch("os.path.exists")
|
|
55
|
+
def test_find_script_with_global_prefix(self, mock_exists, mock_get_config):
|
|
56
|
+
"""Finds script with global/ prefix."""
|
|
57
|
+
mock_config = MagicMock()
|
|
58
|
+
mock_config.local_prompt_templates_dir = "/templates"
|
|
59
|
+
mock_get_config.return_value = mock_config
|
|
60
|
+
mock_exists.return_value = True
|
|
61
|
+
|
|
62
|
+
finder = ScriptFinder()
|
|
63
|
+
result = finder.find_script("global/test.py")
|
|
64
|
+
|
|
65
|
+
expected_path = os.path.join("/templates", "global-scripts", "test.py")
|
|
66
|
+
assert result == expected_path
|
|
67
|
+
|
|
68
|
+
@patch("ara_cli.chat_script_runner.script_finder.ConfigManager.get_config")
|
|
69
|
+
@patch("os.path.exists")
|
|
70
|
+
def test_find_script_in_custom_first(self, mock_exists, mock_get_config):
|
|
71
|
+
"""Finds script in custom-scripts first."""
|
|
72
|
+
mock_config = MagicMock()
|
|
73
|
+
mock_config.local_prompt_templates_dir = "/templates"
|
|
74
|
+
mock_get_config.return_value = mock_config
|
|
75
|
+
|
|
76
|
+
# Custom script exists
|
|
77
|
+
def exists_side_effect(path):
|
|
78
|
+
return "custom-scripts" in path
|
|
79
|
+
|
|
80
|
+
mock_exists.side_effect = exists_side_effect
|
|
81
|
+
|
|
82
|
+
finder = ScriptFinder()
|
|
83
|
+
result = finder.find_script("test.py")
|
|
84
|
+
|
|
85
|
+
assert "custom-scripts" in result
|
|
86
|
+
|
|
87
|
+
@patch("ara_cli.chat_script_runner.script_finder.ConfigManager.get_config")
|
|
88
|
+
@patch("os.path.exists")
|
|
89
|
+
def test_find_script_falls_back_to_global(self, mock_exists, mock_get_config):
|
|
90
|
+
"""Falls back to global-scripts when not in custom."""
|
|
91
|
+
mock_config = MagicMock()
|
|
92
|
+
mock_config.local_prompt_templates_dir = "/templates"
|
|
93
|
+
mock_get_config.return_value = mock_config
|
|
94
|
+
|
|
95
|
+
# Only global script exists
|
|
96
|
+
def exists_side_effect(path):
|
|
97
|
+
return "global-scripts" in path
|
|
98
|
+
|
|
99
|
+
mock_exists.side_effect = exists_side_effect
|
|
100
|
+
|
|
101
|
+
finder = ScriptFinder()
|
|
102
|
+
result = finder.find_script("test.py")
|
|
103
|
+
|
|
104
|
+
assert "global-scripts" in result
|
|
105
|
+
|
|
106
|
+
@patch("ara_cli.chat_script_runner.script_finder.ConfigManager.get_config")
|
|
107
|
+
@patch("os.path.exists", return_value=False)
|
|
108
|
+
def test_find_script_returns_none_when_not_found(
|
|
109
|
+
self, mock_exists, mock_get_config
|
|
110
|
+
):
|
|
111
|
+
"""Returns None when script not found."""
|
|
112
|
+
mock_config = MagicMock()
|
|
113
|
+
mock_config.local_prompt_templates_dir = "/templates"
|
|
114
|
+
mock_get_config.return_value = mock_config
|
|
115
|
+
|
|
116
|
+
finder = ScriptFinder()
|
|
117
|
+
result = finder.find_script("nonexistent.py")
|
|
118
|
+
|
|
119
|
+
assert result is None
|
|
120
|
+
|
|
121
|
+
@patch("ara_cli.chat_script_runner.script_finder.ConfigManager.get_config")
|
|
122
|
+
def test_stores_absolute_path_on_init(self, mock_get_config):
|
|
123
|
+
"""Stores absolute path at init time, enabling script discovery after chdir.
|
|
124
|
+
|
|
125
|
+
This test reproduces the bug where 'ara prompt chat' couldn't find custom
|
|
126
|
+
scripts because Chat.start() changes the working directory and the relative
|
|
127
|
+
path './ara/.araconfig' no longer resolved correctly.
|
|
128
|
+
"""
|
|
129
|
+
mock_config = MagicMock()
|
|
130
|
+
mock_config.local_prompt_templates_dir = "./ara/.araconfig"
|
|
131
|
+
mock_get_config.return_value = mock_config
|
|
132
|
+
|
|
133
|
+
# Create finder from original working directory
|
|
134
|
+
original_cwd = os.getcwd()
|
|
135
|
+
finder = ScriptFinder()
|
|
136
|
+
|
|
137
|
+
# Verify it stored an absolute path
|
|
138
|
+
assert os.path.isabs(finder.local_prompt_templates_dir)
|
|
139
|
+
|
|
140
|
+
# The path should resolve to cwd + relative path
|
|
141
|
+
expected_abs = os.path.abspath("./ara/.araconfig")
|
|
142
|
+
assert finder.local_prompt_templates_dir == expected_abs
|
|
143
|
+
|
|
144
|
+
@patch("ara_cli.chat_script_runner.script_finder.ConfigManager.get_config")
|
|
145
|
+
def test_scripts_found_after_chdir(self, mock_get_config):
|
|
146
|
+
"""Scripts are found even after changing working directory.
|
|
147
|
+
|
|
148
|
+
Simulates the 'ara prompt chat' scenario where the working directory
|
|
149
|
+
changes to the artefact data directory after ScriptFinder is created.
|
|
150
|
+
"""
|
|
151
|
+
# Use a temp directory to simulate the scenario
|
|
152
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
153
|
+
# Setup: Create directories simulating project structure
|
|
154
|
+
project_root = os.path.join(tmpdir, "project")
|
|
155
|
+
custom_scripts_dir = os.path.join(project_root, "ara", ".araconfig", "custom-scripts")
|
|
156
|
+
artefact_data_dir = os.path.join(project_root, "ara", "capabilities", "test.data")
|
|
157
|
+
os.makedirs(custom_scripts_dir)
|
|
158
|
+
os.makedirs(artefact_data_dir)
|
|
159
|
+
|
|
160
|
+
# Create a test script
|
|
161
|
+
test_script = os.path.join(custom_scripts_dir, "test_script.py")
|
|
162
|
+
with open(test_script, "w") as f:
|
|
163
|
+
f.write("print('hello')")
|
|
164
|
+
|
|
165
|
+
original_cwd = os.getcwd()
|
|
166
|
+
try:
|
|
167
|
+
# Change to project root (simulating ara cli startup)
|
|
168
|
+
os.chdir(project_root)
|
|
169
|
+
|
|
170
|
+
mock_config = MagicMock()
|
|
171
|
+
mock_config.local_prompt_templates_dir = "./ara/.araconfig"
|
|
172
|
+
mock_get_config.return_value = mock_config
|
|
173
|
+
|
|
174
|
+
# Create ScriptFinder while in project root
|
|
175
|
+
finder = ScriptFinder()
|
|
176
|
+
|
|
177
|
+
# Now change to artefact data dir (simulating Chat.start())
|
|
178
|
+
os.chdir(artefact_data_dir)
|
|
179
|
+
|
|
180
|
+
# ScriptFinder should still find the script
|
|
181
|
+
result = finder.find_script("test_script.py")
|
|
182
|
+
assert result is not None
|
|
183
|
+
assert result == test_script
|
|
184
|
+
finally:
|
|
185
|
+
os.chdir(original_cwd)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# Tests for ScriptLister
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TestScriptLister:
|
|
194
|
+
"""Tests for ScriptLister class."""
|
|
195
|
+
|
|
196
|
+
@patch("ara_cli.chat_script_runner.script_lister.ScriptFinder")
|
|
197
|
+
@patch("os.path.isdir", return_value=True)
|
|
198
|
+
@patch("glob.glob")
|
|
199
|
+
def test_get_custom_scripts(self, mock_glob, mock_isdir, mock_finder_class):
|
|
200
|
+
"""Returns list of custom script basenames."""
|
|
201
|
+
mock_finder = MagicMock()
|
|
202
|
+
mock_finder.get_custom_scripts_dir.return_value = "/templates/custom-scripts"
|
|
203
|
+
mock_finder_class.return_value = mock_finder
|
|
204
|
+
mock_glob.return_value = [
|
|
205
|
+
"/templates/custom-scripts/script1.py",
|
|
206
|
+
"/templates/custom-scripts/script2.py",
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
lister = ScriptLister()
|
|
210
|
+
result = lister.get_custom_scripts()
|
|
211
|
+
|
|
212
|
+
assert result == ["script1.py", "script2.py"]
|
|
213
|
+
|
|
214
|
+
@patch("ara_cli.chat_script_runner.script_lister.ScriptFinder")
|
|
215
|
+
@patch("os.path.isdir", return_value=True)
|
|
216
|
+
@patch("glob.glob")
|
|
217
|
+
def test_get_global_scripts(self, mock_glob, mock_isdir, mock_finder_class):
|
|
218
|
+
"""Returns list of global script basenames."""
|
|
219
|
+
mock_finder = MagicMock()
|
|
220
|
+
mock_finder.get_global_scripts_dir.return_value = "/templates/global-scripts"
|
|
221
|
+
mock_finder_class.return_value = mock_finder
|
|
222
|
+
mock_glob.return_value = ["/templates/global-scripts/global1.py"]
|
|
223
|
+
|
|
224
|
+
lister = ScriptLister()
|
|
225
|
+
result = lister.get_global_scripts()
|
|
226
|
+
|
|
227
|
+
assert result == ["global1.py"]
|
|
228
|
+
|
|
229
|
+
@patch("ara_cli.chat_script_runner.script_lister.ScriptFinder")
|
|
230
|
+
@patch("os.path.isdir", return_value=False)
|
|
231
|
+
def test_get_custom_scripts_returns_empty_when_dir_not_exists(
|
|
232
|
+
self, mock_isdir, mock_finder_class
|
|
233
|
+
):
|
|
234
|
+
"""Returns empty list when custom scripts dir doesn't exist."""
|
|
235
|
+
mock_finder = MagicMock()
|
|
236
|
+
mock_finder.get_custom_scripts_dir.return_value = "/nonexistent"
|
|
237
|
+
mock_finder_class.return_value = mock_finder
|
|
238
|
+
|
|
239
|
+
lister = ScriptLister()
|
|
240
|
+
result = lister.get_custom_scripts()
|
|
241
|
+
|
|
242
|
+
assert result == []
|
|
243
|
+
|
|
244
|
+
@patch("ara_cli.chat_script_runner.script_lister.ScriptFinder")
|
|
245
|
+
@patch("os.path.isdir", return_value=True)
|
|
246
|
+
@patch("glob.glob")
|
|
247
|
+
def test_get_all_scripts_combines_and_prefixes(
|
|
248
|
+
self, mock_glob, mock_isdir, mock_finder_class
|
|
249
|
+
):
|
|
250
|
+
"""Combines custom and global scripts with global/ prefix."""
|
|
251
|
+
mock_finder = MagicMock()
|
|
252
|
+
mock_finder.get_custom_scripts_dir.return_value = "/templates/custom-scripts"
|
|
253
|
+
mock_finder.get_global_scripts_dir.return_value = "/templates/global-scripts"
|
|
254
|
+
mock_finder_class.return_value = mock_finder
|
|
255
|
+
|
|
256
|
+
def glob_side_effect(pattern):
|
|
257
|
+
if "custom" in pattern:
|
|
258
|
+
return ["/templates/custom-scripts/custom.py"]
|
|
259
|
+
return ["/templates/global-scripts/global.py"]
|
|
260
|
+
|
|
261
|
+
mock_glob.side_effect = glob_side_effect
|
|
262
|
+
|
|
263
|
+
lister = ScriptLister()
|
|
264
|
+
result = lister.get_all_scripts()
|
|
265
|
+
|
|
266
|
+
assert "custom.py" in result
|
|
267
|
+
assert "global/global.py" in result
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# =============================================================================
|
|
271
|
+
# Tests for ScriptRunner
|
|
272
|
+
# =============================================================================
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class TestScriptRunner:
|
|
276
|
+
"""Tests for ScriptRunner class."""
|
|
277
|
+
|
|
278
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptFinder")
|
|
279
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptLister")
|
|
280
|
+
def test_run_script_returns_error_when_not_found(
|
|
281
|
+
self, mock_lister, mock_finder_class
|
|
282
|
+
):
|
|
283
|
+
"""Returns error message when script not found."""
|
|
284
|
+
mock_finder = MagicMock()
|
|
285
|
+
mock_finder.find_script.return_value = None
|
|
286
|
+
mock_finder_class.return_value = mock_finder
|
|
287
|
+
|
|
288
|
+
runner = ScriptRunner(chat_instance=MagicMock())
|
|
289
|
+
result = runner.run_script("nonexistent.py")
|
|
290
|
+
|
|
291
|
+
assert "not found" in result
|
|
292
|
+
|
|
293
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptFinder")
|
|
294
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptLister")
|
|
295
|
+
@patch("subprocess.run")
|
|
296
|
+
def test_run_script_returns_stdout_on_success(
|
|
297
|
+
self, mock_run, mock_lister, mock_finder_class
|
|
298
|
+
):
|
|
299
|
+
"""Returns stdout when script runs successfully."""
|
|
300
|
+
mock_finder = MagicMock()
|
|
301
|
+
mock_finder.find_script.return_value = "/path/to/script.py"
|
|
302
|
+
mock_finder_class.return_value = mock_finder
|
|
303
|
+
|
|
304
|
+
mock_result = MagicMock()
|
|
305
|
+
mock_result.stdout = "Script output"
|
|
306
|
+
mock_run.return_value = mock_result
|
|
307
|
+
|
|
308
|
+
runner = ScriptRunner(chat_instance=MagicMock())
|
|
309
|
+
result = runner.run_script("script.py")
|
|
310
|
+
|
|
311
|
+
assert result == "Script output"
|
|
312
|
+
|
|
313
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptFinder")
|
|
314
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptLister")
|
|
315
|
+
@patch("subprocess.run")
|
|
316
|
+
def test_run_script_with_args(
|
|
317
|
+
self, mock_run, mock_lister, mock_finder_class
|
|
318
|
+
):
|
|
319
|
+
"""Passes arguments to the script."""
|
|
320
|
+
mock_finder = MagicMock()
|
|
321
|
+
mock_finder.find_script.return_value = "/path/to/script.py"
|
|
322
|
+
mock_finder_class.return_value = mock_finder
|
|
323
|
+
|
|
324
|
+
mock_result = MagicMock()
|
|
325
|
+
mock_result.stdout = "Output with args"
|
|
326
|
+
mock_run.return_value = mock_result
|
|
327
|
+
|
|
328
|
+
runner = ScriptRunner(chat_instance=MagicMock())
|
|
329
|
+
result = runner.run_script("script.py", args=["arg1", "arg2"])
|
|
330
|
+
|
|
331
|
+
mock_run.assert_called_with(
|
|
332
|
+
["python", "/path/to/script.py", "arg1", "arg2"],
|
|
333
|
+
capture_output=True,
|
|
334
|
+
text=True,
|
|
335
|
+
check=True,
|
|
336
|
+
)
|
|
337
|
+
assert result == "Output with args"
|
|
338
|
+
|
|
339
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptFinder")
|
|
340
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptLister")
|
|
341
|
+
@patch("subprocess.run")
|
|
342
|
+
def test_run_script_returns_error_on_failure(
|
|
343
|
+
self, mock_run, mock_lister, mock_finder_class
|
|
344
|
+
):
|
|
345
|
+
"""Returns error message when script fails."""
|
|
346
|
+
import subprocess
|
|
347
|
+
|
|
348
|
+
mock_finder = MagicMock()
|
|
349
|
+
mock_finder.find_script.return_value = "/path/to/script.py"
|
|
350
|
+
mock_finder_class.return_value = mock_finder
|
|
351
|
+
|
|
352
|
+
mock_run.side_effect = subprocess.CalledProcessError(
|
|
353
|
+
1, "python", stderr="Error details"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
runner = ScriptRunner(chat_instance=MagicMock())
|
|
357
|
+
result = runner.run_script("script.py")
|
|
358
|
+
|
|
359
|
+
assert "Error running script" in result
|
|
360
|
+
|
|
361
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptFinder")
|
|
362
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptLister")
|
|
363
|
+
def test_get_available_scripts(self, mock_lister_class, mock_finder):
|
|
364
|
+
"""Returns all available scripts."""
|
|
365
|
+
mock_lister = MagicMock()
|
|
366
|
+
mock_lister.get_all_scripts.return_value = ["script1.py", "script2.py"]
|
|
367
|
+
mock_lister_class.return_value = mock_lister
|
|
368
|
+
|
|
369
|
+
runner = ScriptRunner(chat_instance=MagicMock())
|
|
370
|
+
result = runner.get_available_scripts()
|
|
371
|
+
|
|
372
|
+
assert result == ["script1.py", "script2.py"]
|
|
373
|
+
|
|
374
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptFinder")
|
|
375
|
+
@patch("ara_cli.chat_script_runner.script_runner.ScriptLister")
|
|
376
|
+
def test_get_global_scripts(self, mock_lister_class, mock_finder):
|
|
377
|
+
"""Returns global scripts."""
|
|
378
|
+
mock_lister = MagicMock()
|
|
379
|
+
mock_lister.get_global_scripts.return_value = ["global.py"]
|
|
380
|
+
mock_lister_class.return_value = mock_lister
|
|
381
|
+
|
|
382
|
+
runner = ScriptRunner(chat_instance=MagicMock())
|
|
383
|
+
result = runner.get_global_scripts()
|
|
384
|
+
|
|
385
|
+
assert result == ["global.py"]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# =============================================================================
|
|
389
|
+
# Tests for ScriptCompleter
|
|
390
|
+
# =============================================================================
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class TestScriptCompleter:
|
|
394
|
+
"""Tests for ScriptCompleter class."""
|
|
395
|
+
|
|
396
|
+
@patch("ara_cli.chat_script_runner.script_completer.ScriptLister")
|
|
397
|
+
def test_completes_all_scripts_by_default(self, mock_lister_class):
|
|
398
|
+
"""Returns all scripts when not global prefix."""
|
|
399
|
+
mock_lister = MagicMock()
|
|
400
|
+
mock_lister.get_all_scripts.return_value = [
|
|
401
|
+
"script1.py",
|
|
402
|
+
"script2.py",
|
|
403
|
+
"global/test.py",
|
|
404
|
+
]
|
|
405
|
+
mock_lister_class.return_value = mock_lister
|
|
406
|
+
|
|
407
|
+
completer = ScriptCompleter()
|
|
408
|
+
result = completer("", "rpy ", 4, 4)
|
|
409
|
+
|
|
410
|
+
assert "script1.py" in result
|
|
411
|
+
assert "script2.py" in result
|
|
412
|
+
|
|
413
|
+
@patch("ara_cli.chat_script_runner.script_completer.ScriptLister")
|
|
414
|
+
def test_filters_scripts_by_prefix(self, mock_lister_class):
|
|
415
|
+
"""Filters scripts by text prefix."""
|
|
416
|
+
mock_lister = MagicMock()
|
|
417
|
+
mock_lister.get_all_scripts.return_value = [
|
|
418
|
+
"script1.py",
|
|
419
|
+
"script2.py",
|
|
420
|
+
"other.py",
|
|
421
|
+
]
|
|
422
|
+
mock_lister_class.return_value = mock_lister
|
|
423
|
+
|
|
424
|
+
completer = ScriptCompleter()
|
|
425
|
+
result = completer("script", "rpy script", 4, 10)
|
|
426
|
+
|
|
427
|
+
assert "script1.py" in result
|
|
428
|
+
assert "script2.py" in result
|
|
429
|
+
assert "other.py" not in result
|
|
430
|
+
|
|
431
|
+
@patch("ara_cli.chat_script_runner.script_completer.ScriptLister")
|
|
432
|
+
def test_completes_global_scripts_with_prefix(self, mock_lister_class):
|
|
433
|
+
"""Returns only global scripts when using global/ prefix."""
|
|
434
|
+
mock_lister = MagicMock()
|
|
435
|
+
mock_lister.get_global_scripts.return_value = ["global1.py", "global2.py"]
|
|
436
|
+
mock_lister_class.return_value = mock_lister
|
|
437
|
+
|
|
438
|
+
completer = ScriptCompleter()
|
|
439
|
+
result = completer("", "rpy global/", 11, 11)
|
|
440
|
+
|
|
441
|
+
assert "global1.py" in result
|
|
442
|
+
assert "global2.py" in result
|
|
443
|
+
|
|
444
|
+
@patch("ara_cli.chat_script_runner.script_completer.ScriptLister")
|
|
445
|
+
def test_returns_all_when_text_empty(self, mock_lister_class):
|
|
446
|
+
"""Returns all scripts when text is empty."""
|
|
447
|
+
mock_lister = MagicMock()
|
|
448
|
+
mock_lister.get_all_scripts.return_value = ["a.py", "b.py"]
|
|
449
|
+
mock_lister_class.return_value = mock_lister
|
|
450
|
+
|
|
451
|
+
completer = ScriptCompleter()
|
|
452
|
+
result = completer("", "rpy ", 4, 4)
|
|
453
|
+
|
|
454
|
+
assert result == ["a.py", "b.py"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
from ara_cli.children_contribution_updater import ChildrenContributionUpdater
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestChildrenContributionUpdater:
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def updater(self):
|
|
10
|
+
return ChildrenContributionUpdater()
|
|
11
|
+
|
|
12
|
+
@patch(
|
|
13
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._find_children"
|
|
14
|
+
)
|
|
15
|
+
@patch(
|
|
16
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._count_children"
|
|
17
|
+
)
|
|
18
|
+
@patch("ara_cli.children_contribution_updater.Classifier.can_have_children")
|
|
19
|
+
@patch(
|
|
20
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._check_target_exists"
|
|
21
|
+
)
|
|
22
|
+
@patch(
|
|
23
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._get_common_valid_parents"
|
|
24
|
+
)
|
|
25
|
+
def test_get_children_info_basic(
|
|
26
|
+
self,
|
|
27
|
+
mock_common_parents,
|
|
28
|
+
mock_check_exists,
|
|
29
|
+
mock_can_have_children,
|
|
30
|
+
mock_count,
|
|
31
|
+
mock_find,
|
|
32
|
+
updater,
|
|
33
|
+
):
|
|
34
|
+
mock_find.return_value = {"task": [MagicMock(title="T1")]}
|
|
35
|
+
mock_count.return_value = 1
|
|
36
|
+
mock_can_have_children.return_value = True
|
|
37
|
+
mock_check_exists.return_value = False
|
|
38
|
+
mock_common_parents.return_value = ["epic"]
|
|
39
|
+
|
|
40
|
+
info = updater.get_children_info("P1", "feature", "story")
|
|
41
|
+
|
|
42
|
+
assert info["has_children"] is True
|
|
43
|
+
assert info["children_count"] == 1
|
|
44
|
+
assert info["target_can_have_children"] is True
|
|
45
|
+
assert info["requires_action"] is False
|
|
46
|
+
assert info["target_exists"] is False
|
|
47
|
+
assert (
|
|
48
|
+
info["message"]
|
|
49
|
+
== "Found 1 children. They will be updated to reference the new classifier."
|
|
50
|
+
)
|
|
51
|
+
assert "task" in info["children"]
|
|
52
|
+
assert info["children"]["task"][0]["title"] == "T1"
|
|
53
|
+
|
|
54
|
+
@patch(
|
|
55
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._find_children"
|
|
56
|
+
)
|
|
57
|
+
@patch(
|
|
58
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._count_children"
|
|
59
|
+
)
|
|
60
|
+
@patch("ara_cli.children_contribution_updater.Classifier.can_have_children")
|
|
61
|
+
@patch(
|
|
62
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._check_target_exists"
|
|
63
|
+
)
|
|
64
|
+
def test_get_children_info_requires_action(
|
|
65
|
+
self, mock_check_exists, mock_can_have_children, mock_count, mock_find, updater
|
|
66
|
+
):
|
|
67
|
+
mock_find.return_value = {"task": []}
|
|
68
|
+
mock_count.return_value = 1
|
|
69
|
+
mock_can_have_children.return_value = False # Target cannot have children
|
|
70
|
+
mock_check_exists.return_value = False
|
|
71
|
+
|
|
72
|
+
info = updater.get_children_info("P1", "feature", "task")
|
|
73
|
+
|
|
74
|
+
assert info["requires_action"] is True
|
|
75
|
+
assert "requires handling 1 children" in info["message"]
|
|
76
|
+
|
|
77
|
+
@patch(
|
|
78
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._find_children"
|
|
79
|
+
)
|
|
80
|
+
@patch(
|
|
81
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._count_children"
|
|
82
|
+
)
|
|
83
|
+
@patch("ara_cli.children_contribution_updater.Classifier.can_have_children")
|
|
84
|
+
@patch(
|
|
85
|
+
"ara_cli.children_contribution_updater.ChildrenContributionUpdater._check_target_exists"
|
|
86
|
+
)
|
|
87
|
+
def test_get_children_info_target_exists(
|
|
88
|
+
self, mock_check_exists, mock_can_have_children, mock_count, mock_find, updater
|
|
89
|
+
):
|
|
90
|
+
mock_find.return_value = {}
|
|
91
|
+
mock_count.return_value = 0
|
|
92
|
+
mock_can_have_children.return_value = True
|
|
93
|
+
mock_check_exists.return_value = True
|
|
94
|
+
|
|
95
|
+
info = updater.get_children_info("P1", "feature", "story")
|
|
96
|
+
|
|
97
|
+
assert info["target_exists"] is True
|
|
98
|
+
assert "already exists" in info["message"]
|