ara-cli 0.1.10.5__py3-none-any.whl → 0.1.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ara_cli/__init__.py +51 -6
- ara_cli/__main__.py +87 -75
- ara_cli/ara_command_action.py +189 -101
- ara_cli/ara_config.py +187 -128
- ara_cli/ara_subcommands/common.py +2 -2
- ara_cli/ara_subcommands/config.py +221 -0
- ara_cli/ara_subcommands/convert.py +107 -0
- ara_cli/ara_subcommands/fetch.py +41 -0
- ara_cli/ara_subcommands/fetch_agents.py +22 -0
- ara_cli/ara_subcommands/fetch_scripts.py +19 -0
- ara_cli/ara_subcommands/fetch_templates.py +15 -10
- ara_cli/ara_subcommands/list.py +97 -23
- ara_cli/ara_subcommands/prompt.py +266 -106
- ara_cli/artefact_autofix.py +117 -64
- ara_cli/artefact_converter.py +355 -0
- ara_cli/artefact_creator.py +41 -17
- ara_cli/artefact_lister.py +3 -3
- ara_cli/artefact_models/artefact_model.py +1 -1
- ara_cli/artefact_models/artefact_templates.py +0 -9
- ara_cli/artefact_models/feature_artefact_model.py +8 -8
- ara_cli/artefact_reader.py +62 -43
- ara_cli/artefact_scan.py +39 -17
- ara_cli/chat.py +300 -71
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_process_manager.py +155 -0
- ara_cli/chat_script_runner/__init__.py +0 -0
- ara_cli/chat_script_runner/script_completer.py +23 -0
- ara_cli/chat_script_runner/script_finder.py +41 -0
- ara_cli/chat_script_runner/script_lister.py +36 -0
- ara_cli/chat_script_runner/script_runner.py +36 -0
- ara_cli/chat_web_search/__init__.py +0 -0
- ara_cli/chat_web_search/web_search.py +263 -0
- ara_cli/children_contribution_updater.py +737 -0
- ara_cli/classifier.py +34 -0
- ara_cli/commands/agent_run_command.py +98 -0
- ara_cli/commands/fetch_agents_command.py +106 -0
- ara_cli/commands/fetch_scripts_command.py +43 -0
- ara_cli/commands/fetch_templates_command.py +39 -0
- ara_cli/commands/fetch_templates_commands.py +39 -0
- ara_cli/commands/list_agents_command.py +39 -0
- ara_cli/commands/load_command.py +4 -3
- ara_cli/commands/load_image_command.py +1 -1
- ara_cli/commands/read_command.py +23 -27
- ara_cli/completers.py +95 -35
- ara_cli/constants.py +2 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +26 -11
- ara_cli/file_loaders/document_reader.py +0 -178
- ara_cli/file_loaders/factories/__init__.py +0 -0
- ara_cli/file_loaders/factories/document_reader_factory.py +32 -0
- ara_cli/file_loaders/factories/file_loader_factory.py +27 -0
- ara_cli/file_loaders/file_loader.py +1 -30
- ara_cli/file_loaders/loaders/__init__.py +0 -0
- ara_cli/file_loaders/{document_file_loader.py → loaders/document_file_loader.py} +1 -1
- ara_cli/file_loaders/loaders/text_file_loader.py +47 -0
- ara_cli/file_loaders/readers/__init__.py +0 -0
- ara_cli/file_loaders/readers/docx_reader.py +49 -0
- ara_cli/file_loaders/readers/excel_reader.py +27 -0
- ara_cli/file_loaders/{markdown_reader.py → readers/markdown_reader.py} +1 -1
- ara_cli/file_loaders/readers/odt_reader.py +59 -0
- ara_cli/file_loaders/readers/pdf_reader.py +54 -0
- ara_cli/file_loaders/readers/pptx_reader.py +104 -0
- ara_cli/file_loaders/tools/__init__.py +0 -0
- ara_cli/llm_utils.py +58 -0
- ara_cli/output_suppressor.py +53 -0
- ara_cli/prompt_chat.py +20 -4
- ara_cli/prompt_extractor.py +47 -32
- ara_cli/prompt_handler.py +123 -17
- ara_cli/tag_extractor.py +8 -7
- ara_cli/template_loader.py +2 -1
- ara_cli/template_manager.py +52 -21
- ara_cli/templates/global-scripts/hello_global.py +1 -0
- ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
- ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
- ara_cli/version.py +1 -1
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/METADATA +49 -11
- ara_cli-0.1.14.0.dist-info/RECORD +253 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/WHEEL +1 -1
- tests/test_ara_command_action.py +31 -19
- tests/test_ara_config.py +177 -90
- tests/test_artefact_autofix.py +170 -97
- tests/test_artefact_autofix_integration.py +495 -0
- tests/test_artefact_converter.py +312 -0
- tests/test_artefact_extraction.py +564 -0
- tests/test_artefact_lister.py +11 -8
- tests/test_chat.py +166 -130
- tests/test_chat_givens_images.py +603 -0
- tests/test_chat_script_runner.py +454 -0
- tests/test_children_contribution_updater.py +98 -0
- tests/test_document_loader_office.py +267 -0
- tests/test_llm_utils.py +164 -0
- tests/test_prompt_chat.py +343 -0
- tests/test_prompt_extractor.py +683 -0
- tests/test_prompt_handler.py +416 -214
- tests/test_setup_default_chat_prompt_mode.py +198 -0
- tests/test_tag_extractor.py +95 -49
- tests/test_web_search.py +467 -0
- ara_cli/file_loaders/document_readers.py +0 -233
- ara_cli/file_loaders/file_loaders.py +0 -123
- ara_cli/file_loaders/text_file_loader.py +0 -187
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/pytest_unittest_prompt.blueprint.md +0 -32
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli-0.1.10.5.dist-info/RECORD +0 -194
- /ara_cli/file_loaders/{binary_file_loader.py → loaders/binary_file_loader.py} +0 -0
- /ara_cli/file_loaders/{image_processor.py → tools/image_processor.py} +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,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()
|
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
|