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.
Files changed (106) hide show
  1. ara_cli/__init__.py +51 -6
  2. ara_cli/__main__.py +87 -75
  3. ara_cli/ara_command_action.py +95 -57
  4. ara_cli/ara_config.py +187 -128
  5. ara_cli/ara_subcommands/common.py +2 -2
  6. ara_cli/ara_subcommands/config.py +221 -0
  7. ara_cli/ara_subcommands/convert.py +43 -0
  8. ara_cli/ara_subcommands/fetch.py +41 -0
  9. ara_cli/ara_subcommands/fetch_agents.py +22 -0
  10. ara_cli/ara_subcommands/fetch_scripts.py +19 -0
  11. ara_cli/ara_subcommands/fetch_templates.py +15 -10
  12. ara_cli/ara_subcommands/list.py +97 -23
  13. ara_cli/artefact_autofix.py +115 -62
  14. ara_cli/artefact_converter.py +256 -0
  15. ara_cli/chat.py +283 -62
  16. ara_cli/chat_agent/__init__.py +0 -0
  17. ara_cli/chat_agent/agent_process_manager.py +155 -0
  18. ara_cli/chat_script_runner/__init__.py +0 -0
  19. ara_cli/chat_script_runner/script_completer.py +23 -0
  20. ara_cli/chat_script_runner/script_finder.py +41 -0
  21. ara_cli/chat_script_runner/script_lister.py +36 -0
  22. ara_cli/chat_script_runner/script_runner.py +36 -0
  23. ara_cli/chat_web_search/__init__.py +0 -0
  24. ara_cli/chat_web_search/web_search.py +263 -0
  25. ara_cli/commands/agent_run_command.py +98 -0
  26. ara_cli/commands/fetch_agents_command.py +106 -0
  27. ara_cli/commands/fetch_scripts_command.py +43 -0
  28. ara_cli/commands/fetch_templates_command.py +39 -0
  29. ara_cli/commands/fetch_templates_commands.py +39 -0
  30. ara_cli/commands/list_agents_command.py +39 -0
  31. ara_cli/completers.py +71 -35
  32. ara_cli/constants.py +2 -0
  33. ara_cli/directory_navigator.py +37 -4
  34. ara_cli/llm_utils.py +58 -0
  35. ara_cli/prompt_chat.py +20 -4
  36. ara_cli/prompt_extractor.py +47 -32
  37. ara_cli/template_loader.py +2 -1
  38. ara_cli/template_manager.py +52 -21
  39. ara_cli/templates/global-scripts/hello_global.py +1 -0
  40. ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
  41. ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
  42. ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
  43. ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
  44. ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
  45. ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
  46. ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
  47. ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
  48. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  49. ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
  50. ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
  51. ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
  52. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  53. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  54. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  55. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  56. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  57. ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
  58. ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
  59. ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
  60. ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
  61. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  62. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  63. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  64. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  65. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  66. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  67. ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
  68. ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
  69. ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
  70. ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
  71. ara_cli/version.py +1 -1
  72. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/METADATA +33 -1
  73. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/RECORD +89 -43
  74. tests/test_ara_command_action.py +31 -19
  75. tests/test_ara_config.py +177 -90
  76. tests/test_artefact_autofix.py +170 -97
  77. tests/test_artefact_autofix_integration.py +495 -0
  78. tests/test_artefact_converter.py +357 -0
  79. tests/test_artefact_extraction.py +564 -0
  80. tests/test_chat.py +162 -126
  81. tests/test_chat_givens_images.py +603 -0
  82. tests/test_chat_script_runner.py +454 -0
  83. tests/test_llm_utils.py +164 -0
  84. tests/test_prompt_chat.py +343 -0
  85. tests/test_prompt_extractor.py +683 -0
  86. tests/test_web_search.py +467 -0
  87. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  88. ara_cli/templates/prompt-modules/blueprints/pytest_unittest_prompt.blueprint.md +0 -32
  89. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  90. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  91. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  92. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  93. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  94. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  95. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  96. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  97. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  98. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  99. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  100. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  101. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  102. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  103. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  104. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/WHEEL +0 -0
  105. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/entry_points.txt +0 -0
  106. {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"]
@@ -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