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,267 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch, ANY
3
+ import sys
4
+ import os
5
+
6
+ # Mock the new dependencies to avoid import errors during test collection if they are not installed
7
+ sys.modules["pandas"] = MagicMock()
8
+ sys.modules["openpyxl"] = MagicMock()
9
+ sys.modules["pptx"] = MagicMock()
10
+ sys.modules["pptx.presentation"] = MagicMock()
11
+ sys.modules["pptx.enum.shapes"] = MagicMock()
12
+
13
+ from ara_cli.file_loaders.readers.excel_reader import ExcelReader
14
+ from ara_cli.file_loaders.readers.pptx_reader import PptxReader
15
+ from ara_cli.file_loaders.factories.document_reader_factory import DocumentReaderFactory
16
+
17
+
18
+ class TestExcelReader(unittest.TestCase):
19
+ @patch("pandas.read_excel")
20
+ def test_read_excel_success(self, mock_read_excel):
21
+ # Setup mock dataframe
22
+ mock_df = MagicMock()
23
+ mock_df.empty = False
24
+ mock_df.to_markdown.return_value = "| Col1 | Col2 |\n|---|---|\n| Val1 | Val2 |"
25
+ mock_df.fillna.return_value = mock_df
26
+
27
+ # Setup read_excel return value (dict of sheets)
28
+ mock_read_excel.return_value = {"Sheet1": mock_df}
29
+
30
+ reader = ExcelReader("test.xlsx")
31
+ content = reader.read()
32
+
33
+ self.assertIn("### Sheet: Sheet1", content)
34
+ self.assertIn("| Col1 | Col2 |", content)
35
+ mock_read_excel.assert_called_once_with("test.xlsx", sheet_name=None)
36
+ mock_df.to_markdown.assert_called_once()
37
+
38
+ @patch("pandas.read_excel")
39
+ def test_read_excel_empty_sheet(self, mock_read_excel):
40
+ mock_df = MagicMock()
41
+ mock_df.empty = True
42
+
43
+ mock_read_excel.return_value = {"SheetEmpty": mock_df}
44
+
45
+ reader = ExcelReader("test.xlsx")
46
+ content = reader.read()
47
+
48
+ self.assertIn("### Sheet: SheetEmpty", content)
49
+ self.assertIn("_Empty Sheet_", content)
50
+
51
+ @patch("pandas.read_excel")
52
+ def test_read_excel_error(self, mock_read_excel):
53
+ mock_read_excel.side_effect = Exception("File not found")
54
+
55
+ reader = ExcelReader("test.xlsx")
56
+ content = reader.read()
57
+
58
+ self.assertIn("Error reading Excel file: File not found", content)
59
+
60
+
61
+ class TestPptxReader(unittest.TestCase):
62
+ @patch("pptx.Presentation")
63
+ def test_read_pptx_success(self, mock_presentation):
64
+ # Setup mock presentation
65
+ mock_prs = MagicMock()
66
+ mock_slide = MagicMock()
67
+
68
+ # Setup shapes
69
+ mock_shape_title = MagicMock()
70
+ mock_shape_title.has_text_frame = True
71
+ mock_shape_title.text_frame.text = "Slide Title"
72
+ mock_shape_title.top = 0
73
+ mock_shape_title.left = 0
74
+
75
+ mock_shape_content = MagicMock()
76
+ mock_shape_content.has_text_frame = True
77
+ mock_shape_content.text_frame.paragraphs = [
78
+ MagicMock(text="Bullet 1"),
79
+ MagicMock(text="Bullet 2"),
80
+ ]
81
+ mock_shape_content.top = 100
82
+ mock_shape_content.left = 0
83
+
84
+ # slide.shapes must be a mock that is iterable AND has a title attribute
85
+ mock_shapes = MagicMock()
86
+ mock_shapes.__iter__.return_value = [mock_shape_title, mock_shape_content]
87
+ mock_shapes.title = mock_shape_title
88
+
89
+ mock_slide.shapes = mock_shapes
90
+
91
+ mock_slide.shapes = mock_shapes
92
+ mock_prs.slides = [mock_slide]
93
+
94
+ mock_presentation.return_value = mock_prs
95
+
96
+ reader = PptxReader("test.pptx")
97
+ content = reader.read()
98
+
99
+ self.assertIn("## Slide 1", content)
100
+ self.assertIn("### Slide Title", content)
101
+ self.assertIn("- Bullet 1", content)
102
+ self.assertIn("- Bullet 2", content)
103
+
104
+ @patch("pptx.Presentation")
105
+ def test_read_pptx_with_images(self, mock_presentation):
106
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
107
+
108
+ # Setup mock presentation
109
+ mock_prs = MagicMock()
110
+ mock_slide = MagicMock()
111
+
112
+ # Setup shapes
113
+ mock_shape_image = MagicMock()
114
+ mock_shape_image.has_text_frame = False
115
+ mock_shape_image.shape_type = MSO_SHAPE_TYPE.PICTURE
116
+ mock_shape_image.image.blob = b"fake_image_data"
117
+ mock_shape_image.image.ext = "png"
118
+ mock_shape_image.top = 0
119
+ mock_shape_image.left = 0
120
+
121
+ # Mock shapes as list is fine here if we don't access .title, but cleaner to be consistent if code checks .title
122
+ mock_shapes = MagicMock()
123
+ mock_shapes.__iter__.return_value = [mock_shape_image]
124
+ # If code checks slide.shapes.title, we should ensure it doesn't crash or has it
125
+ # The code does: try: if shape == slide.shapes.title: ... except AttributeError: ...
126
+ # So it handles missing title attribute.
127
+
128
+ mock_slide.shapes = mock_shapes
129
+ mock_prs.slides = [mock_slide]
130
+ mock_presentation.return_value = mock_prs
131
+
132
+ # Mock create_image_data_dir and save_and_describe_image
133
+ with patch.object(
134
+ PptxReader, "create_image_data_dir"
135
+ ) as mock_create_dir, patch.object(
136
+ PptxReader, "save_and_describe_image"
137
+ ) as mock_save_describe:
138
+
139
+ mock_create_dir.return_value = "fake/dir"
140
+ mock_save_describe.return_value = (
141
+ "fake/dir/1.png",
142
+ "A fake image description",
143
+ )
144
+
145
+ reader = PptxReader("test.pptx")
146
+ content = reader.read(extract_images=True)
147
+
148
+ self.assertIn("## Slide 1", content)
149
+ self.assertIn("Image: fake/dir/1.png", content)
150
+ self.assertIn("[A fake image description]", content)
151
+
152
+ @patch("pptx.Presentation")
153
+ def test_read_pptx_groups_and_placeholders(self, mock_presentation):
154
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
155
+
156
+ mock_prs = MagicMock()
157
+ mock_slide = MagicMock()
158
+
159
+ # 1. Group Shape containing an image
160
+ mock_group = MagicMock()
161
+ mock_group.shape_type = MSO_SHAPE_TYPE.GROUP
162
+
163
+ mock_sub_image = MagicMock()
164
+ mock_sub_image.shape_type = MSO_SHAPE_TYPE.PICTURE
165
+ mock_sub_image.image.blob = b"group_img"
166
+ mock_sub_image.image.ext = "jpg"
167
+
168
+ mock_group.shapes = [mock_sub_image]
169
+ mock_group.top = 0
170
+ mock_group.left = 0
171
+
172
+ # 2. Placeholder with image
173
+ mock_placeholder = MagicMock()
174
+ mock_placeholder.shape_type = 0 # Not PICTURE
175
+ mock_placeholder.is_placeholder = True
176
+ mock_placeholder.image.blob = b"place_img"
177
+ mock_placeholder.image.ext = "png"
178
+ mock_placeholder.top = 100
179
+ mock_placeholder.left = 0
180
+
181
+ # 3. Shape that raises error on image access (code robustness check)
182
+ mock_broken_image = MagicMock()
183
+ mock_broken_image.shape_type = MSO_SHAPE_TYPE.PICTURE
184
+ type(mock_broken_image.image).blob = unittest.mock.PropertyMock(
185
+ side_effect=Exception("Corrupt")
186
+ )
187
+ mock_broken_image.top = 200
188
+ mock_broken_image.left = 0
189
+
190
+ # 4. Shape with text that looks like a title but raises AttributeError (line 47 coverage)
191
+ mock_title_ish = MagicMock()
192
+ mock_title_ish.has_text_frame = True
193
+ mock_title_ish.text_frame.text = "A Title"
194
+ mock_title_ish.text_frame.paragraphs = [MagicMock(text="A Title")]
195
+ mock_title_ish.is_placeholder = (
196
+ False # Important: default valid MagicMock is truthy!
197
+ )
198
+ # Ensure checking slide.shapes.title raises AttributeError
199
+ mock_slide.shapes.title = unittest.mock.PropertyMock(
200
+ side_effect=AttributeError("No title")
201
+ )
202
+ mock_title_ish.top = 300
203
+ mock_title_ish.left = 0
204
+
205
+ # Configure slide shapes iteration
206
+ mock_slide.shapes.__iter__.return_value = [
207
+ mock_group,
208
+ mock_placeholder,
209
+ mock_broken_image,
210
+ mock_title_ish,
211
+ ]
212
+
213
+ mock_prs.slides = [mock_slide]
214
+ mock_presentation.return_value = mock_prs
215
+
216
+ with patch.object(
217
+ PptxReader, "create_image_data_dir", return_value="dir"
218
+ ), patch.object(
219
+ PptxReader, "save_and_describe_image", return_value=("path", "desc")
220
+ ):
221
+
222
+ reader = PptxReader("test.pptx")
223
+ # Force AttributeError on slide.shapes.title access during loop
224
+ # The code does: `if shape == slide.shapes.title:`
225
+ # We need checking `slide.shapes.title` to fail
226
+ type(mock_slide.shapes).title = unittest.mock.PropertyMock(
227
+ side_effect=AttributeError
228
+ )
229
+
230
+ content = reader.read(extract_images=True)
231
+
232
+ # Should match group image
233
+ # Should match placeholder image
234
+ # Should handle broken image gracefully (print warning)
235
+ # Should handle title attribute error (continue)
236
+
237
+ # Since save_and_describe_image is mocked, we just check count or content existence
238
+ # We expect 2 valid images (group + placeholder)
239
+ self.assertEqual(content.count("Image: path"), 2)
240
+ # Since AttributeError on title check, correct behavior is falling back to paragraph iteration
241
+ self.assertIn("- A Title", content)
242
+
243
+ @patch("pptx.Presentation")
244
+ def test_read_pptx_top_level_error(self, mock_presentation):
245
+ mock_presentation.side_effect = Exception("File corrupt")
246
+ reader = PptxReader("test.pptx")
247
+ content = reader.read()
248
+ self.assertEqual(content, "Error reading PowerPoint file: File corrupt")
249
+
250
+
251
+ class TestDocumentReaderFactory(unittest.TestCase):
252
+ def test_create_reader(self):
253
+ from ara_cli.file_loaders.readers.docx_reader import DocxReader
254
+
255
+ self.assertIsInstance(
256
+ DocumentReaderFactory.create_reader("test.xlsx"), ExcelReader
257
+ )
258
+ self.assertIsInstance(
259
+ DocumentReaderFactory.create_reader("test.pptx"), PptxReader
260
+ )
261
+ self.assertIsInstance(
262
+ DocumentReaderFactory.create_reader("test.DOCX"), DocxReader
263
+ )
264
+
265
+
266
+ if __name__ == "__main__":
267
+ unittest.main()
@@ -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