ara-cli 0.1.10.5__py3-none-any.whl → 0.1.13.3__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 +95 -57
- 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 +43 -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/artefact_autofix.py +115 -62
- ara_cli/artefact_converter.py +256 -0
- ara_cli/chat.py +283 -62
- 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/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/completers.py +71 -35
- ara_cli/constants.py +2 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/llm_utils.py +58 -0
- ara_cli/prompt_chat.py +20 -4
- ara_cli/prompt_extractor.py +47 -32
- 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.13.3.dist-info}/METADATA +33 -1
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/RECORD +89 -43
- 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 +357 -0
- tests/test_artefact_extraction.py +564 -0
- tests/test_chat.py +162 -126
- tests/test_chat_givens_images.py +603 -0
- tests/test_chat_script_runner.py +454 -0
- tests/test_llm_utils.py +164 -0
- tests/test_prompt_chat.py +343 -0
- tests/test_prompt_extractor.py +683 -0
- tests/test_web_search.py +467 -0
- 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 → ara_cli-0.1.13.3.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.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"]
|
tests/test_llm_utils.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for llm_utils.py
|
|
3
|
+
|
|
4
|
+
Provides full test coverage for LLM utility functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import patch, MagicMock
|
|
9
|
+
from ara_cli.llm_utils import (
|
|
10
|
+
get_configured_conversion_llm_model,
|
|
11
|
+
create_pydantic_ai_agent,
|
|
12
|
+
FALLBACK_MODEL,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Tests for get_configured_conversion_llm_model
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestGetConfiguredConversionLlmModel:
|
|
22
|
+
"""Tests for get_configured_conversion_llm_model function."""
|
|
23
|
+
|
|
24
|
+
@patch("ara_cli.llm_utils.ConfigManager.get_config")
|
|
25
|
+
def test_returns_fallback_when_no_config(self, mock_get_config):
|
|
26
|
+
"""Returns fallback model when config is missing."""
|
|
27
|
+
mock_get_config.side_effect = Exception("Config not found")
|
|
28
|
+
|
|
29
|
+
result = get_configured_conversion_llm_model()
|
|
30
|
+
|
|
31
|
+
assert result == FALLBACK_MODEL
|
|
32
|
+
|
|
33
|
+
@patch("ara_cli.llm_utils.ConfigManager.get_config")
|
|
34
|
+
def test_returns_fallback_when_conversion_llm_not_set(self, mock_get_config):
|
|
35
|
+
"""Returns fallback when conversion_llm is not set."""
|
|
36
|
+
mock_config = MagicMock()
|
|
37
|
+
mock_config.conversion_llm = None
|
|
38
|
+
mock_get_config.return_value = mock_config
|
|
39
|
+
|
|
40
|
+
result = get_configured_conversion_llm_model()
|
|
41
|
+
|
|
42
|
+
assert result == FALLBACK_MODEL
|
|
43
|
+
|
|
44
|
+
@patch("ara_cli.llm_utils.ConfigManager.get_config")
|
|
45
|
+
def test_returns_fallback_when_key_not_in_llm_config(self, mock_get_config):
|
|
46
|
+
"""Returns fallback when conversion_llm key not in llm_config."""
|
|
47
|
+
mock_config = MagicMock()
|
|
48
|
+
mock_config.conversion_llm = "nonexistent_key"
|
|
49
|
+
mock_config.llm_config = {}
|
|
50
|
+
mock_get_config.return_value = mock_config
|
|
51
|
+
|
|
52
|
+
result = get_configured_conversion_llm_model()
|
|
53
|
+
|
|
54
|
+
assert result == FALLBACK_MODEL
|
|
55
|
+
|
|
56
|
+
@patch("ara_cli.llm_utils.ConfigManager.get_config")
|
|
57
|
+
def test_converts_litellm_format_to_pydantic_format(self, mock_get_config):
|
|
58
|
+
"""Converts LiteLLM model format (/) to PydanticAI format (:)."""
|
|
59
|
+
mock_config = MagicMock()
|
|
60
|
+
mock_config.conversion_llm = "default"
|
|
61
|
+
mock_llm_item = MagicMock()
|
|
62
|
+
mock_llm_item.model = "openai/gpt-4o"
|
|
63
|
+
mock_config.llm_config = {"default": mock_llm_item}
|
|
64
|
+
mock_get_config.return_value = mock_config
|
|
65
|
+
|
|
66
|
+
result = get_configured_conversion_llm_model()
|
|
67
|
+
|
|
68
|
+
assert result == "openai:gpt-4o"
|
|
69
|
+
|
|
70
|
+
@patch("ara_cli.llm_utils.ConfigManager.get_config")
|
|
71
|
+
def test_keeps_pydantic_format_unchanged(self, mock_get_config):
|
|
72
|
+
"""Keeps PydanticAI format unchanged."""
|
|
73
|
+
mock_config = MagicMock()
|
|
74
|
+
mock_config.conversion_llm = "default"
|
|
75
|
+
mock_llm_item = MagicMock()
|
|
76
|
+
mock_llm_item.model = "openai:gpt-4o"
|
|
77
|
+
mock_config.llm_config = {"default": mock_llm_item}
|
|
78
|
+
mock_get_config.return_value = mock_config
|
|
79
|
+
|
|
80
|
+
result = get_configured_conversion_llm_model()
|
|
81
|
+
|
|
82
|
+
assert result == "openai:gpt-4o"
|
|
83
|
+
|
|
84
|
+
@patch("ara_cli.llm_utils.ConfigManager.get_config")
|
|
85
|
+
def test_handles_model_without_prefix(self, mock_get_config):
|
|
86
|
+
"""Handles model name without provider prefix."""
|
|
87
|
+
mock_config = MagicMock()
|
|
88
|
+
mock_config.conversion_llm = "default"
|
|
89
|
+
mock_llm_item = MagicMock()
|
|
90
|
+
mock_llm_item.model = "gpt-4o"
|
|
91
|
+
mock_config.llm_config = {"default": mock_llm_item}
|
|
92
|
+
mock_get_config.return_value = mock_config
|
|
93
|
+
|
|
94
|
+
result = get_configured_conversion_llm_model()
|
|
95
|
+
|
|
96
|
+
assert result == "gpt-4o"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# =============================================================================
|
|
100
|
+
# Tests for create_pydantic_ai_agent
|
|
101
|
+
# =============================================================================
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestCreatePydanticAiAgent:
|
|
105
|
+
"""Tests for create_pydantic_ai_agent function."""
|
|
106
|
+
|
|
107
|
+
@patch("ara_cli.llm_utils.Agent")
|
|
108
|
+
@patch("ara_cli.llm_utils.get_configured_conversion_llm_model")
|
|
109
|
+
def test_uses_configured_model_when_not_provided(self, mock_get_model, mock_agent):
|
|
110
|
+
"""Uses configured model when model_name not provided."""
|
|
111
|
+
mock_get_model.return_value = "configured:model"
|
|
112
|
+
mock_output_type = MagicMock()
|
|
113
|
+
|
|
114
|
+
create_pydantic_ai_agent(output_type=mock_output_type)
|
|
115
|
+
|
|
116
|
+
mock_agent.assert_called_once_with(
|
|
117
|
+
model="configured:model",
|
|
118
|
+
output_type=mock_output_type,
|
|
119
|
+
instrument=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@patch("ara_cli.llm_utils.Agent")
|
|
123
|
+
@patch("ara_cli.llm_utils.get_configured_conversion_llm_model")
|
|
124
|
+
def test_uses_provided_model_name(self, mock_get_model, mock_agent):
|
|
125
|
+
"""Uses provided model_name instead of configured model."""
|
|
126
|
+
mock_output_type = MagicMock()
|
|
127
|
+
|
|
128
|
+
create_pydantic_ai_agent(
|
|
129
|
+
output_type=mock_output_type, model_name="custom:model"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
mock_agent.assert_called_once_with(
|
|
133
|
+
model="custom:model",
|
|
134
|
+
output_type=mock_output_type,
|
|
135
|
+
instrument=True,
|
|
136
|
+
)
|
|
137
|
+
mock_get_model.assert_not_called()
|
|
138
|
+
|
|
139
|
+
@patch("ara_cli.llm_utils.Agent")
|
|
140
|
+
@patch("ara_cli.llm_utils.get_configured_conversion_llm_model")
|
|
141
|
+
def test_sets_instrument_flag(self, mock_get_model, mock_agent):
|
|
142
|
+
"""Sets instrument flag correctly."""
|
|
143
|
+
mock_get_model.return_value = "test:model"
|
|
144
|
+
mock_output_type = MagicMock()
|
|
145
|
+
|
|
146
|
+
create_pydantic_ai_agent(output_type=mock_output_type, instrument=False)
|
|
147
|
+
|
|
148
|
+
mock_agent.assert_called_once_with(
|
|
149
|
+
model="test:model",
|
|
150
|
+
output_type=mock_output_type,
|
|
151
|
+
instrument=False,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@patch("ara_cli.llm_utils.Agent")
|
|
155
|
+
@patch("ara_cli.llm_utils.get_configured_conversion_llm_model")
|
|
156
|
+
def test_returns_agent_instance(self, mock_get_model, mock_agent):
|
|
157
|
+
"""Returns the created agent instance."""
|
|
158
|
+
mock_get_model.return_value = "test:model"
|
|
159
|
+
mock_agent_instance = MagicMock()
|
|
160
|
+
mock_agent.return_value = mock_agent_instance
|
|
161
|
+
|
|
162
|
+
result = create_pydantic_ai_agent(output_type=MagicMock())
|
|
163
|
+
|
|
164
|
+
assert result == mock_agent_instance
|