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.
Files changed (151) hide show
  1. ara_cli/__init__.py +51 -6
  2. ara_cli/__main__.py +87 -75
  3. ara_cli/ara_command_action.py +189 -101
  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 +107 -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/ara_subcommands/prompt.py +266 -106
  14. ara_cli/artefact_autofix.py +117 -64
  15. ara_cli/artefact_converter.py +355 -0
  16. ara_cli/artefact_creator.py +41 -17
  17. ara_cli/artefact_lister.py +3 -3
  18. ara_cli/artefact_models/artefact_model.py +1 -1
  19. ara_cli/artefact_models/artefact_templates.py +0 -9
  20. ara_cli/artefact_models/feature_artefact_model.py +8 -8
  21. ara_cli/artefact_reader.py +62 -43
  22. ara_cli/artefact_scan.py +39 -17
  23. ara_cli/chat.py +300 -71
  24. ara_cli/chat_agent/__init__.py +0 -0
  25. ara_cli/chat_agent/agent_process_manager.py +155 -0
  26. ara_cli/chat_script_runner/__init__.py +0 -0
  27. ara_cli/chat_script_runner/script_completer.py +23 -0
  28. ara_cli/chat_script_runner/script_finder.py +41 -0
  29. ara_cli/chat_script_runner/script_lister.py +36 -0
  30. ara_cli/chat_script_runner/script_runner.py +36 -0
  31. ara_cli/chat_web_search/__init__.py +0 -0
  32. ara_cli/chat_web_search/web_search.py +263 -0
  33. ara_cli/children_contribution_updater.py +737 -0
  34. ara_cli/classifier.py +34 -0
  35. ara_cli/commands/agent_run_command.py +98 -0
  36. ara_cli/commands/fetch_agents_command.py +106 -0
  37. ara_cli/commands/fetch_scripts_command.py +43 -0
  38. ara_cli/commands/fetch_templates_command.py +39 -0
  39. ara_cli/commands/fetch_templates_commands.py +39 -0
  40. ara_cli/commands/list_agents_command.py +39 -0
  41. ara_cli/commands/load_command.py +4 -3
  42. ara_cli/commands/load_image_command.py +1 -1
  43. ara_cli/commands/read_command.py +23 -27
  44. ara_cli/completers.py +95 -35
  45. ara_cli/constants.py +2 -0
  46. ara_cli/directory_navigator.py +37 -4
  47. ara_cli/error_handler.py +26 -11
  48. ara_cli/file_loaders/document_reader.py +0 -178
  49. ara_cli/file_loaders/factories/__init__.py +0 -0
  50. ara_cli/file_loaders/factories/document_reader_factory.py +32 -0
  51. ara_cli/file_loaders/factories/file_loader_factory.py +27 -0
  52. ara_cli/file_loaders/file_loader.py +1 -30
  53. ara_cli/file_loaders/loaders/__init__.py +0 -0
  54. ara_cli/file_loaders/{document_file_loader.py → loaders/document_file_loader.py} +1 -1
  55. ara_cli/file_loaders/loaders/text_file_loader.py +47 -0
  56. ara_cli/file_loaders/readers/__init__.py +0 -0
  57. ara_cli/file_loaders/readers/docx_reader.py +49 -0
  58. ara_cli/file_loaders/readers/excel_reader.py +27 -0
  59. ara_cli/file_loaders/{markdown_reader.py → readers/markdown_reader.py} +1 -1
  60. ara_cli/file_loaders/readers/odt_reader.py +59 -0
  61. ara_cli/file_loaders/readers/pdf_reader.py +54 -0
  62. ara_cli/file_loaders/readers/pptx_reader.py +104 -0
  63. ara_cli/file_loaders/tools/__init__.py +0 -0
  64. ara_cli/llm_utils.py +58 -0
  65. ara_cli/output_suppressor.py +53 -0
  66. ara_cli/prompt_chat.py +20 -4
  67. ara_cli/prompt_extractor.py +47 -32
  68. ara_cli/prompt_handler.py +123 -17
  69. ara_cli/tag_extractor.py +8 -7
  70. ara_cli/template_loader.py +2 -1
  71. ara_cli/template_manager.py +52 -21
  72. ara_cli/templates/global-scripts/hello_global.py +1 -0
  73. ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
  74. ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
  75. ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
  76. ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
  77. ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
  78. ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
  79. ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
  80. ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
  81. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  82. ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
  83. ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
  84. ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
  85. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  86. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  87. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  88. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  89. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  90. ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
  91. ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
  92. ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
  93. ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
  94. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  95. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  96. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  97. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  98. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  99. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  100. ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
  101. ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
  102. ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
  103. ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
  104. ara_cli/version.py +1 -1
  105. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/METADATA +49 -11
  106. ara_cli-0.1.14.0.dist-info/RECORD +253 -0
  107. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/WHEEL +1 -1
  108. tests/test_ara_command_action.py +31 -19
  109. tests/test_ara_config.py +177 -90
  110. tests/test_artefact_autofix.py +170 -97
  111. tests/test_artefact_autofix_integration.py +495 -0
  112. tests/test_artefact_converter.py +312 -0
  113. tests/test_artefact_extraction.py +564 -0
  114. tests/test_artefact_lister.py +11 -8
  115. tests/test_chat.py +166 -130
  116. tests/test_chat_givens_images.py +603 -0
  117. tests/test_chat_script_runner.py +454 -0
  118. tests/test_children_contribution_updater.py +98 -0
  119. tests/test_document_loader_office.py +267 -0
  120. tests/test_llm_utils.py +164 -0
  121. tests/test_prompt_chat.py +343 -0
  122. tests/test_prompt_extractor.py +683 -0
  123. tests/test_prompt_handler.py +416 -214
  124. tests/test_setup_default_chat_prompt_mode.py +198 -0
  125. tests/test_tag_extractor.py +95 -49
  126. tests/test_web_search.py +467 -0
  127. ara_cli/file_loaders/document_readers.py +0 -233
  128. ara_cli/file_loaders/file_loaders.py +0 -123
  129. ara_cli/file_loaders/text_file_loader.py +0 -187
  130. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  131. ara_cli/templates/prompt-modules/blueprints/pytest_unittest_prompt.blueprint.md +0 -32
  132. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  133. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  134. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  135. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  136. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  137. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  138. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  139. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  140. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  141. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  142. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  143. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  144. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  145. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  146. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  147. ara_cli-0.1.10.5.dist-info/RECORD +0 -194
  148. /ara_cli/file_loaders/{binary_file_loader.py → loaders/binary_file_loader.py} +0 -0
  149. /ara_cli/file_loaders/{image_processor.py → tools/image_processor.py} +0 -0
  150. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/entry_points.txt +0 -0
  151. {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"]