ara-cli 0.1.9.69__py3-none-any.whl → 0.1.10.8__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.

Potentially problematic release.


This version of ara-cli might be problematic. Click here for more details.

Files changed (150) hide show
  1. ara_cli/__init__.py +18 -2
  2. ara_cli/__main__.py +248 -62
  3. ara_cli/ara_command_action.py +155 -86
  4. ara_cli/ara_config.py +226 -80
  5. ara_cli/ara_subcommands/__init__.py +0 -0
  6. ara_cli/ara_subcommands/autofix.py +26 -0
  7. ara_cli/ara_subcommands/chat.py +27 -0
  8. ara_cli/ara_subcommands/classifier_directory.py +16 -0
  9. ara_cli/ara_subcommands/common.py +100 -0
  10. ara_cli/ara_subcommands/create.py +75 -0
  11. ara_cli/ara_subcommands/delete.py +22 -0
  12. ara_cli/ara_subcommands/extract.py +22 -0
  13. ara_cli/ara_subcommands/fetch_templates.py +14 -0
  14. ara_cli/ara_subcommands/list.py +65 -0
  15. ara_cli/ara_subcommands/list_tags.py +25 -0
  16. ara_cli/ara_subcommands/load.py +48 -0
  17. ara_cli/ara_subcommands/prompt.py +136 -0
  18. ara_cli/ara_subcommands/read.py +47 -0
  19. ara_cli/ara_subcommands/read_status.py +20 -0
  20. ara_cli/ara_subcommands/read_user.py +20 -0
  21. ara_cli/ara_subcommands/reconnect.py +27 -0
  22. ara_cli/ara_subcommands/rename.py +22 -0
  23. ara_cli/ara_subcommands/scan.py +14 -0
  24. ara_cli/ara_subcommands/set_status.py +22 -0
  25. ara_cli/ara_subcommands/set_user.py +22 -0
  26. ara_cli/ara_subcommands/template.py +16 -0
  27. ara_cli/artefact_autofix.py +649 -68
  28. ara_cli/artefact_creator.py +8 -11
  29. ara_cli/artefact_deleter.py +2 -4
  30. ara_cli/artefact_fuzzy_search.py +22 -10
  31. ara_cli/artefact_link_updater.py +4 -4
  32. ara_cli/artefact_lister.py +29 -55
  33. ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
  34. ara_cli/artefact_models/artefact_load.py +11 -3
  35. ara_cli/artefact_models/artefact_model.py +146 -39
  36. ara_cli/artefact_models/artefact_templates.py +70 -44
  37. ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
  38. ara_cli/artefact_models/epic_artefact_model.py +34 -26
  39. ara_cli/artefact_models/feature_artefact_model.py +203 -64
  40. ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
  41. ara_cli/artefact_models/serialize_helper.py +1 -1
  42. ara_cli/artefact_models/task_artefact_model.py +83 -15
  43. ara_cli/artefact_models/userstory_artefact_model.py +37 -27
  44. ara_cli/artefact_models/vision_artefact_model.py +23 -42
  45. ara_cli/artefact_reader.py +92 -91
  46. ara_cli/artefact_renamer.py +8 -4
  47. ara_cli/artefact_scan.py +66 -3
  48. ara_cli/chat.py +622 -162
  49. ara_cli/chat_agent/__init__.py +0 -0
  50. ara_cli/chat_agent/agent_communicator.py +62 -0
  51. ara_cli/chat_agent/agent_process_manager.py +211 -0
  52. ara_cli/chat_agent/agent_status_manager.py +73 -0
  53. ara_cli/chat_agent/agent_workspace_manager.py +76 -0
  54. ara_cli/commands/__init__.py +0 -0
  55. ara_cli/commands/command.py +7 -0
  56. ara_cli/commands/extract_command.py +15 -0
  57. ara_cli/commands/load_command.py +65 -0
  58. ara_cli/commands/load_image_command.py +34 -0
  59. ara_cli/commands/read_command.py +117 -0
  60. ara_cli/completers.py +144 -0
  61. ara_cli/directory_navigator.py +37 -4
  62. ara_cli/error_handler.py +134 -0
  63. ara_cli/file_classifier.py +6 -5
  64. ara_cli/file_lister.py +1 -1
  65. ara_cli/file_loaders/__init__.py +0 -0
  66. ara_cli/file_loaders/binary_file_loader.py +33 -0
  67. ara_cli/file_loaders/document_file_loader.py +34 -0
  68. ara_cli/file_loaders/document_reader.py +245 -0
  69. ara_cli/file_loaders/document_readers.py +233 -0
  70. ara_cli/file_loaders/file_loader.py +50 -0
  71. ara_cli/file_loaders/file_loaders.py +123 -0
  72. ara_cli/file_loaders/image_processor.py +89 -0
  73. ara_cli/file_loaders/markdown_reader.py +75 -0
  74. ara_cli/file_loaders/text_file_loader.py +187 -0
  75. ara_cli/global_file_lister.py +51 -0
  76. ara_cli/list_filter.py +1 -1
  77. ara_cli/output_suppressor.py +1 -1
  78. ara_cli/prompt_extractor.py +215 -88
  79. ara_cli/prompt_handler.py +521 -134
  80. ara_cli/prompt_rag.py +2 -2
  81. ara_cli/tag_extractor.py +83 -38
  82. ara_cli/template_loader.py +245 -0
  83. ara_cli/template_manager.py +18 -13
  84. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  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/prompt_template_tech_stack_transformer.commands.md +95 -0
  91. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  92. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  93. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  94. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  95. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  96. ara_cli/update_config_prompt.py +9 -3
  97. ara_cli/version.py +1 -1
  98. ara_cli-0.1.10.8.dist-info/METADATA +241 -0
  99. ara_cli-0.1.10.8.dist-info/RECORD +193 -0
  100. tests/test_ara_command_action.py +73 -59
  101. tests/test_ara_config.py +341 -36
  102. tests/test_artefact_autofix.py +1060 -0
  103. tests/test_artefact_link_updater.py +3 -3
  104. tests/test_artefact_lister.py +52 -132
  105. tests/test_artefact_renamer.py +2 -2
  106. tests/test_artefact_scan.py +327 -33
  107. tests/test_chat.py +2063 -498
  108. tests/test_file_classifier.py +24 -1
  109. tests/test_file_creator.py +3 -5
  110. tests/test_file_lister.py +1 -1
  111. tests/test_global_file_lister.py +131 -0
  112. tests/test_list_filter.py +2 -2
  113. tests/test_prompt_handler.py +746 -0
  114. tests/test_tag_extractor.py +19 -13
  115. tests/test_template_loader.py +192 -0
  116. tests/test_template_manager.py +5 -4
  117. tests/test_update_config_prompt.py +2 -2
  118. ara_cli/ara_command_parser.py +0 -327
  119. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  120. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  121. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  122. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  123. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  124. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  125. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  126. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  127. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  128. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  129. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  130. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  131. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  132. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  133. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  134. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  135. ara_cli/templates/template.businessgoal +0 -10
  136. ara_cli/templates/template.capability +0 -10
  137. ara_cli/templates/template.epic +0 -15
  138. ara_cli/templates/template.example +0 -6
  139. ara_cli/templates/template.feature +0 -26
  140. ara_cli/templates/template.issue +0 -14
  141. ara_cli/templates/template.keyfeature +0 -15
  142. ara_cli/templates/template.task +0 -6
  143. ara_cli/templates/template.userstory +0 -17
  144. ara_cli/templates/template.vision +0 -14
  145. ara_cli-0.1.9.69.dist-info/METADATA +0 -16
  146. ara_cli-0.1.9.69.dist-info/RECORD +0 -158
  147. tests/test_ara_autofix.py +0 -219
  148. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
  149. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
  150. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1060 @@
1
+ import pytest
2
+ from unittest.mock import patch, mock_open, MagicMock
3
+ from ara_cli.error_handler import AraError
4
+ from ara_cli.artefact_autofix import (
5
+ read_report_file,
6
+ parse_report,
7
+ apply_autofix,
8
+ read_artefact,
9
+ determine_artefact_type_and_class,
10
+ run_agent,
11
+ write_corrected_artefact,
12
+ construct_prompt,
13
+ fix_title_mismatch,
14
+ ask_for_correct_contribution,
15
+ ask_for_contribution_choice,
16
+ _has_valid_contribution,
17
+ set_closest_contribution,
18
+ fix_contribution,
19
+ fix_rule,
20
+ fix_scenario_placeholder_mismatch,
21
+ _extract_scenario_block,
22
+ _is_scenario_boundary,
23
+ _process_scenario_block,
24
+ _get_line_indentation,
25
+ _extract_placeholders_from_scenario,
26
+ _update_docstring_state,
27
+ _convert_to_scenario_outline,
28
+ _create_examples_table,
29
+ populate_classified_artefact_info,
30
+ should_skip_issue,
31
+ determine_attempt_count,
32
+ apply_deterministic_fix,
33
+ apply_non_deterministic_fix,
34
+ attempt_autofix_loop,
35
+ )
36
+ from ara_cli.artefact_models.artefact_model import Artefact, ArtefactType, Contribution
37
+
38
+
39
+ @pytest.fixture
40
+ def mock_artefact_type():
41
+ """Provides a mock for the ArtefactType enum member."""
42
+ mock_type = MagicMock()
43
+ mock_type.value = "feature"
44
+ return mock_type
45
+
46
+
47
+ @pytest.fixture
48
+ def mock_artefact_class():
49
+ """Provides a mock for the Artefact class."""
50
+ mock_class = MagicMock()
51
+ mock_class._title_prefix.return_value = "Feature:"
52
+ # Mock the serialize method for the agent tests
53
+ mock_class.serialize.return_value = "llm corrected content"
54
+ return mock_class
55
+
56
+
57
+ @pytest.fixture
58
+ def mock_classified_artefact_info():
59
+ """Provides a mock for the classified artefact info dictionary."""
60
+ return MagicMock()
61
+
62
+
63
+ @pytest.fixture
64
+ def mock_artefact_with_contribution():
65
+ """Provides a mock Artefact with a mock Contribution."""
66
+ mock_contribution = MagicMock(spec=Contribution)
67
+ mock_contribution.artefact_name = "some_artefact"
68
+ mock_contribution.classifier = "feature"
69
+ mock_contribution.rule = "some rule"
70
+
71
+ mock_artefact = MagicMock(spec=Artefact)
72
+ mock_artefact.contribution = mock_contribution
73
+ mock_artefact.title = "my_test_artefact"
74
+ mock_artefact._artefact_type.return_value.value = "requirement"
75
+ mock_artefact.serialize.return_value = "serialized artefact text"
76
+
77
+ return mock_artefact
78
+
79
+
80
+ @pytest.fixture
81
+ def mock_contribution():
82
+ m = MagicMock()
83
+ m.artefact_name = "parent_name"
84
+ m.classifier = "feature"
85
+ m.rule = "my_rule"
86
+ return m
87
+
88
+ @pytest.fixture
89
+ def mock_artefact(mock_contribution):
90
+ m = MagicMock()
91
+ m.contribution = mock_contribution
92
+ m._artefact_type.return_value.value = "requirement"
93
+ m.title = "my_title"
94
+ m.serialize.return_value = "serialized-text"
95
+ return m
96
+
97
+
98
+ def test_read_report_file_success():
99
+ """Tests successful reading of the report file."""
100
+ mock_content = "# Artefact Check Report\n- `file.feature`: reason"
101
+ with patch("builtins.open", mock_open(read_data=mock_content)) as m:
102
+ content = read_report_file()
103
+ assert content == mock_content
104
+ m.assert_called_once_with(
105
+ "incompatible_artefacts_report.md", "r", encoding="utf-8"
106
+ )
107
+
108
+
109
+ def test_read_report_file_not_found(capsys):
110
+ with patch("builtins.open", side_effect=OSError("File not found")):
111
+ content = read_report_file()
112
+ assert content is None
113
+ assert "Artefact scan results file not found" in capsys.readouterr().out
114
+
115
+
116
+ def test_parse_report_with_issues():
117
+ content = (
118
+ "# Artefact Check Report\n\n## feature\n- `path/to/file.feature`: A reason\n"
119
+ )
120
+ expected = {"feature": [("path/to/file.feature", "A reason")]}
121
+ assert parse_report(content) == expected
122
+
123
+
124
+ def test_parse_report_no_issues():
125
+ content = "# Artefact Check Report\n\nNo problems found.\n"
126
+ assert parse_report(content) == {}
127
+
128
+
129
+ def test_parse_report_invalid_format():
130
+ assert parse_report("This is not a valid report") == {}
131
+
132
+
133
+ def test_parse_report_invalid_line_format():
134
+ content = "# Artefact Check Report\n\n## feature\n- an invalid line\n"
135
+ assert parse_report(content) == {"feature": []}
136
+
137
+
138
+ def test_read_artefact_success():
139
+ mock_content = "Feature: My Feature"
140
+ with patch("builtins.open", mock_open(read_data=mock_content)) as m:
141
+ content = read_artefact("file.feature")
142
+ assert content == mock_content
143
+ m.assert_called_once_with("file.feature", "r", encoding="utf-8")
144
+
145
+
146
+ def test_read_artefact_file_not_found(capsys):
147
+ with patch("builtins.open", side_effect=FileNotFoundError):
148
+ result = read_artefact("nonexistent.feature")
149
+ assert result is None
150
+ assert "File not found: nonexistent.feature" in capsys.readouterr().out
151
+
152
+
153
+ @patch("ara_cli.artefact_models.artefact_mapping.artefact_type_mapping")
154
+ def test_determine_artefact_type_and_class_no_class_found(mock_mapping, capsys):
155
+ mock_mapping.get.return_value = None
156
+ with pytest.raises(AraError):
157
+ artefact_type, artefact_class = determine_artefact_type_and_class("feature")
158
+
159
+
160
+ @patch("ara_cli.artefact_models.artefact_model.ArtefactType", side_effect=ValueError)
161
+ def test_determine_artefact_type_and_class_invalid(mock_artefact_type_enum, capsys):
162
+ artefact_type, artefact_class = determine_artefact_type_and_class(
163
+ "invalid_classifier"
164
+ )
165
+ assert artefact_type is None
166
+ assert artefact_class is None
167
+ assert "Invalid classifier: invalid_classifier" in capsys.readouterr().out
168
+
169
+
170
+ def test_write_corrected_artefact():
171
+ with patch("builtins.open", mock_open()) as m:
172
+ write_corrected_artefact("file.feature", "corrected content")
173
+ m.assert_called_once_with("file.feature", "w", encoding="utf-8")
174
+ m().write.assert_called_once_with("corrected content")
175
+
176
+
177
+ def test_construct_prompt_for_task():
178
+ prompt = construct_prompt(ArtefactType.task, "some reason", "file.task", "text")
179
+ assert (
180
+ "For task artefacts, if the action items looks like template or empty"
181
+ in prompt
182
+ )
183
+
184
+
185
+ @patch("ara_cli.artefact_autofix.run_agent")
186
+ @patch(
187
+ "ara_cli.artefact_autofix.determine_artefact_type_and_class",
188
+ return_value=(None, None),
189
+ )
190
+ @patch("ara_cli.artefact_autofix.read_artefact")
191
+ def test_apply_autofix_exits_when_classifier_is_invalid(
192
+ mock_read, mock_determine, mock_run_agent, mock_classified_artefact_info
193
+ ):
194
+ """Tests that apply_autofix exits early if the classifier is invalid."""
195
+ result = apply_autofix(
196
+ file_path="file.feature",
197
+ classifier="invalid",
198
+ reason="reason",
199
+ deterministic=True,
200
+ non_deterministic=True,
201
+ classified_artefact_info=mock_classified_artefact_info,
202
+ )
203
+ assert result is False
204
+ mock_determine.assert_called_once_with("invalid")
205
+ mock_read.assert_not_called()
206
+ mock_run_agent.assert_not_called()
207
+
208
+
209
+ @patch("ara_cli.artefact_autofix.FileClassifier")
210
+ @patch("ara_cli.artefact_autofix.check_file")
211
+ @patch("ara_cli.artefact_autofix.run_agent")
212
+ @patch("ara_cli.artefact_autofix.write_corrected_artefact")
213
+ @patch("ara_cli.artefact_autofix.fix_title_mismatch")
214
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
215
+ @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
216
+ def test_apply_autofix_for_title_mismatch_with_deterministic_flag(
217
+ mock_read,
218
+ mock_determine,
219
+ mock_fix_title,
220
+ mock_write,
221
+ mock_run_agent,
222
+ mock_check_file,
223
+ mock_file_classifier,
224
+ mock_artefact_type,
225
+ mock_artefact_class,
226
+ mock_classified_artefact_info,
227
+ ):
228
+ """Tests that a deterministic fix is applied when the flag is True."""
229
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
230
+ mock_check_file.side_effect = [
231
+ (False, "Filename-Title Mismatch: some details"),
232
+ (True, ""),
233
+ ]
234
+ mock_fix_title.return_value = "fixed text"
235
+
236
+ result = apply_autofix(
237
+ file_path="file.feature",
238
+ classifier="feature",
239
+ reason="Filename-Title Mismatch: some details",
240
+ deterministic=True,
241
+ non_deterministic=False,
242
+ classified_artefact_info=mock_classified_artefact_info,
243
+ )
244
+
245
+ assert result is True
246
+ assert mock_check_file.call_count == 2
247
+ mock_fix_title.assert_called_once_with(
248
+ file_path="file.feature",
249
+ artefact_text="original text",
250
+ artefact_class=mock_artefact_class,
251
+ classified_artefact_info=mock_classified_artefact_info,
252
+ )
253
+ mock_write.assert_called_once_with("file.feature", "fixed text")
254
+ mock_run_agent.assert_not_called()
255
+ mock_file_classifier.assert_called_once()
256
+
257
+
258
+ @patch("ara_cli.artefact_autofix.check_file")
259
+ @patch("ara_cli.artefact_autofix.run_agent")
260
+ @patch("ara_cli.artefact_autofix.write_corrected_artefact")
261
+ @patch("ara_cli.artefact_autofix.fix_title_mismatch")
262
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
263
+ @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
264
+ def test_apply_autofix_skips_title_mismatch_without_deterministic_flag(
265
+ mock_read,
266
+ mock_determine,
267
+ mock_fix_title,
268
+ mock_write,
269
+ mock_run_agent,
270
+ mock_check_file,
271
+ mock_artefact_type,
272
+ mock_artefact_class,
273
+ mock_classified_artefact_info,
274
+ ):
275
+ """Tests that a deterministic fix is skipped when the flag is False."""
276
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
277
+ mock_check_file.return_value = (False, "Filename-Title Mismatch: some details")
278
+
279
+ result = apply_autofix(
280
+ file_path="file.feature",
281
+ classifier="feature",
282
+ reason="Filename-Title Mismatch: some details",
283
+ deterministic=False,
284
+ non_deterministic=True,
285
+ classified_artefact_info=mock_classified_artefact_info,
286
+ )
287
+
288
+ assert result is False
289
+ mock_check_file.assert_called_once()
290
+ mock_read.assert_called_once_with("file.feature")
291
+ mock_fix_title.assert_not_called()
292
+ mock_write.assert_not_called()
293
+ mock_run_agent.assert_not_called()
294
+
295
+
296
+ @patch("ara_cli.artefact_autofix.FileClassifier")
297
+ @patch("ara_cli.artefact_autofix.check_file")
298
+ @patch("ara_cli.artefact_autofix.write_corrected_artefact")
299
+ @patch("ara_cli.artefact_autofix.run_agent")
300
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
301
+ @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
302
+ def test_apply_autofix_for_llm_fix_with_non_deterministic_flag(
303
+ mock_read,
304
+ mock_determine,
305
+ mock_run_agent,
306
+ mock_write,
307
+ mock_check_file,
308
+ mock_file_classifier,
309
+ mock_artefact_type,
310
+ mock_artefact_class,
311
+ mock_classified_artefact_info,
312
+ ):
313
+ """Tests that an LLM fix is applied when the non-deterministic flag is True."""
314
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
315
+ mock_check_file.side_effect = [(False, "Pydantic validation error"), (True, "")]
316
+ mock_run_agent.return_value = mock_artefact_class
317
+
318
+ result = apply_autofix(
319
+ file_path="file.feature",
320
+ classifier="feature",
321
+ reason="Pydantic validation error",
322
+ deterministic=False,
323
+ non_deterministic=True,
324
+ classified_artefact_info=mock_classified_artefact_info,
325
+ )
326
+
327
+ assert result is True
328
+ assert mock_check_file.call_count == 2
329
+ mock_read.assert_called_once_with("file.feature")
330
+ mock_run_agent.assert_called_once()
331
+ mock_write.assert_called_once_with("file.feature", "llm corrected content")
332
+ mock_file_classifier.assert_called_once()
333
+
334
+
335
+ @patch("ara_cli.artefact_autofix.write_corrected_artefact")
336
+ @patch("ara_cli.artefact_autofix.run_agent")
337
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
338
+ @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
339
+ def test_apply_autofix_skips_llm_fix_without_non_deterministic_flag(
340
+ mock_read,
341
+ mock_determine,
342
+ mock_run_agent,
343
+ mock_write,
344
+ mock_artefact_type,
345
+ mock_artefact_class,
346
+ mock_classified_artefact_info,
347
+ ):
348
+ """Tests that an LLM fix is skipped when the non-deterministic flag is False."""
349
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
350
+ reason = "Pydantic validation error"
351
+
352
+ result = apply_autofix(
353
+ "file.feature",
354
+ "feature",
355
+ reason,
356
+ deterministic=True,
357
+ non_deterministic=False,
358
+ classified_artefact_info=mock_classified_artefact_info,
359
+ )
360
+
361
+ assert result is False
362
+ mock_run_agent.assert_not_called()
363
+ mock_write.assert_not_called()
364
+
365
+
366
+ @patch("ara_cli.artefact_autofix.run_agent", side_effect=Exception("LLM failed"))
367
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
368
+ @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
369
+ def test_apply_autofix_llm_exception(
370
+ mock_read,
371
+ mock_determine,
372
+ mock_run_agent,
373
+ capsys,
374
+ mock_artefact_type,
375
+ mock_artefact_class,
376
+ mock_classified_artefact_info,
377
+ ):
378
+ """Tests that an exception during an LLM fix is handled gracefully."""
379
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
380
+ reason = "Pydantic validation error"
381
+
382
+ result = apply_autofix(
383
+ "file.feature",
384
+ "feature",
385
+ reason,
386
+ deterministic=False,
387
+ non_deterministic=True,
388
+ classified_artefact_info=mock_classified_artefact_info,
389
+ )
390
+
391
+ assert result is False
392
+ assert (
393
+ "LLM agent failed to fix artefact at file.feature: LLM failed"
394
+ in capsys.readouterr().out
395
+ )
396
+
397
+
398
+ def test_fix_title_mismatch_success(mock_artefact_class):
399
+ artefact_text = "Feature: wrong title\nSome other content"
400
+ file_path = "path/to/correct_title.feature"
401
+
402
+ expected_text = "Feature: correct title\nSome other content"
403
+
404
+ result = fix_title_mismatch(file_path, artefact_text, mock_artefact_class)
405
+
406
+ assert result == expected_text
407
+ mock_artefact_class._title_prefix.assert_called_once()
408
+
409
+
410
+ def test_fix_title_mismatch_prefix_not_found(capsys, mock_artefact_class):
411
+ artefact_text = "No title prefix here"
412
+ file_path = "path/to/correct_title.feature"
413
+
414
+ result = fix_title_mismatch(file_path, artefact_text, mock_artefact_class)
415
+
416
+ assert result == artefact_text # Should return original text
417
+ assert "Warning: Title prefix 'Feature:' not found" in capsys.readouterr().out
418
+
419
+
420
+ @patch("pydantic_ai.Agent")
421
+ def test_run_agent_exception_handling(mock_agent_class):
422
+ mock_agent_instance = mock_agent_class.return_value
423
+ mock_agent_instance.run_sync.side_effect = Exception("Agent error")
424
+ with pytest.raises(Exception, match="Agent error"):
425
+ run_agent("prompt", MagicMock())
426
+
427
+
428
+ @patch("builtins.input", side_effect=["1"])
429
+ def test_ask_for_contribution_choice_valid(mock_input):
430
+ """Tests selecting a valid choice."""
431
+ choices = ["choice1", "choice2"]
432
+ # This simpler call now works without causing a TypeError
433
+ result = ask_for_contribution_choice(choices)
434
+ assert result == "choice1"
435
+
436
+ @patch("builtins.input", side_effect=["99"])
437
+ def test_ask_for_contribution_choice_out_of_range(mock_input, capsys):
438
+ """Tests selecting a choice that is out of range."""
439
+ choices = ["choice1", "choice2"]
440
+ result = ask_for_contribution_choice(choices)
441
+ assert result is None
442
+ assert "Invalid choice" in capsys.readouterr().out
443
+
444
+
445
+ @patch("builtins.input", side_effect=["not a number"])
446
+ def test_ask_for_contribution_choice_invalid_input(mock_input, capsys):
447
+ """Tests providing non-numeric input."""
448
+ choices = ["choice1", "choice2"]
449
+ result = ask_for_contribution_choice(choices)
450
+ assert result is None
451
+ assert "Invalid input" in capsys.readouterr().out
452
+
453
+
454
+ @patch("builtins.input", side_effect=["feature my_feature_name"])
455
+ def test_ask_for_correct_contribution_valid(mock_input):
456
+ """Tests providing valid '<classifier> <name>' input."""
457
+ name, classifier = ask_for_correct_contribution(("old_name", "feature"))
458
+ assert name == "my_feature_name"
459
+ assert classifier == "feature"
460
+
461
+
462
+ @patch("builtins.input", side_effect=[""])
463
+ def test_ask_for_correct_contribution_empty_input(mock_input):
464
+ """Tests providing empty input."""
465
+ name, classifier = ask_for_correct_contribution()
466
+ assert name is None
467
+ assert classifier is None
468
+
469
+
470
+ @patch("builtins.input", side_effect=["invalid-one-word-input"])
471
+ def test_ask_for_correct_contribution_invalid_format(mock_input, capsys):
472
+ """Tests providing input with the wrong format."""
473
+ # Fix: Use input that results in a single part after split()
474
+ name, classifier = ask_for_correct_contribution()
475
+ assert name is None
476
+ assert classifier is None
477
+ assert "Invalid input format" in capsys.readouterr().out
478
+
479
+
480
+ def test_has_valid_contribution_true(mock_artefact_with_contribution):
481
+ """Tests with a valid contribution object."""
482
+ # Fix: Check for truthiness, not strict boolean equality
483
+ assert _has_valid_contribution(mock_artefact_with_contribution)
484
+
485
+
486
+ def test_has_valid_contribution_false_no_contribution():
487
+ """Tests when the artefact's contribution is None."""
488
+ mock_artefact = MagicMock(spec=Artefact)
489
+ mock_artefact.contribution = None
490
+ # Fix: Check for falsiness, not strict boolean equality
491
+ assert not _has_valid_contribution(mock_artefact)
492
+
493
+
494
+ @patch("ara_cli.artefact_autofix.FileClassifier")
495
+ @patch("ara_cli.artefact_autofix.extract_artefact_names_of_classifier")
496
+ @patch("ara_cli.artefact_autofix.find_closest_name_matches")
497
+ def test_set_closest_contribution_no_change_needed(
498
+ mock_find, mock_extract, mock_classifier, mock_artefact_with_contribution
499
+ ):
500
+ """Tests the case where the contribution name is already the best match."""
501
+ mock_find.return_value = ["some_artefact"] # Exact match is found
502
+ artefact, changed = set_closest_contribution(mock_artefact_with_contribution)
503
+ assert changed is False
504
+ assert artefact == mock_artefact_with_contribution
505
+
506
+
507
+ @patch("ara_cli.artefact_autofix.FileClassifier")
508
+ @patch("ara_cli.artefact_autofix.extract_artefact_names_of_classifier")
509
+ @patch("ara_cli.artefact_autofix.find_closest_name_matches", return_value=[])
510
+ @patch(
511
+ "ara_cli.artefact_autofix.ask_for_correct_contribution",
512
+ return_value=("new_name", "new_classifier"),
513
+ )
514
+ def test_set_closest_contribution_no_matches_user_provides(
515
+ mock_ask, mock_find, mock_extract, mock_classifier, mock_artefact_with_contribution
516
+ ):
517
+ """Tests when no matches are found and the user provides a new contribution."""
518
+ artefact, changed = set_closest_contribution(mock_artefact_with_contribution)
519
+ assert changed is True
520
+ assert artefact.contribution.artefact_name == "new_name"
521
+ assert artefact.contribution.classifier == "new_classifier"
522
+
523
+
524
+ @patch("ara_cli.artefact_autofix.set_closest_contribution")
525
+ @patch("ara_cli.artefact_autofix.FileClassifier")
526
+ def test_fix_contribution(
527
+ mock_file_classifier, mock_set, mock_artefact_with_contribution
528
+ ):
529
+ """Tests the fix_contribution wrapper function."""
530
+ # Arrange
531
+ mock_artefact_class = MagicMock()
532
+ mock_artefact_class.deserialize.return_value = mock_artefact_with_contribution
533
+ mock_set.return_value = (mock_artefact_with_contribution, True)
534
+
535
+ # Act
536
+ result = fix_contribution(
537
+ file_path="dummy.path",
538
+ artefact_text="original text",
539
+ artefact_class=mock_artefact_class,
540
+ classified_artefact_info={},
541
+ )
542
+
543
+ # Assert
544
+ assert result == "serialized artefact text"
545
+ mock_artefact_class.deserialize.assert_called_once_with("original text")
546
+ mock_set.assert_called_once_with(mock_artefact_with_contribution)
547
+
548
+
549
+ @patch("ara_cli.artefact_autofix.FileClassifier")
550
+ @patch("ara_cli.artefact_autofix.check_file")
551
+ @patch("ara_cli.artefact_autofix.write_corrected_artefact")
552
+ @patch("ara_cli.artefact_autofix.fix_contribution", return_value="fixed text")
553
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
554
+ @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
555
+ def test_apply_autofix_for_contribution_mismatch(
556
+ mock_read,
557
+ mock_determine,
558
+ mock_fix_contribution,
559
+ mock_write,
560
+ mock_check_file,
561
+ mock_classifier,
562
+ mock_artefact_type,
563
+ mock_artefact_class,
564
+ mock_classified_artefact_info,
565
+ ):
566
+ """Tests the deterministic fix for 'Invalid Contribution Reference'."""
567
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
568
+ mock_check_file.side_effect = [
569
+ (False, "Invalid Contribution Reference"),
570
+ (True, ""),
571
+ ]
572
+
573
+ result = apply_autofix(
574
+ file_path="file.feature",
575
+ classifier="feature",
576
+ reason="Invalid Contribution Reference",
577
+ classified_artefact_info=mock_classified_artefact_info,
578
+ )
579
+
580
+ assert result is True
581
+ mock_fix_contribution.assert_called_once()
582
+ mock_write.assert_called_once_with("file.feature", "fixed text")
583
+
584
+
585
+ @patch("ara_cli.artefact_autofix.check_file")
586
+ @patch("ara_cli.artefact_autofix.write_corrected_artefact")
587
+ @patch("ara_cli.artefact_autofix.fix_title_mismatch", return_value="original text")
588
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
589
+ @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
590
+ def test_apply_autofix_stops_if_no_alteration(
591
+ mock_read,
592
+ mock_determine,
593
+ mock_fix_title,
594
+ mock_write,
595
+ mock_check_file,
596
+ capsys,
597
+ mock_artefact_type,
598
+ mock_artefact_class,
599
+ mock_classified_artefact_info,
600
+ ):
601
+ """Tests that the loop stops if a fix attempt does not change the file content."""
602
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
603
+ mock_check_file.return_value = (False, "Filename-Title Mismatch")
604
+
605
+ result = apply_autofix(
606
+ file_path="file.feature",
607
+ classifier="feature",
608
+ reason="any",
609
+ classified_artefact_info=mock_classified_artefact_info,
610
+ )
611
+
612
+ assert result is False
613
+ mock_fix_title.assert_called_once()
614
+ mock_write.assert_not_called()
615
+ assert (
616
+ "Fixing attempt did not alter the file. Stopping to prevent infinite loop."
617
+ in capsys.readouterr().out
618
+ )
619
+
620
+
621
+ @patch("ara_cli.artefact_autofix.check_file")
622
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
623
+ def test_apply_autofix_single_pass(
624
+ mock_determine,
625
+ mock_check_file,
626
+ capsys,
627
+ mock_artefact_type,
628
+ mock_artefact_class,
629
+ mock_classified_artefact_info,
630
+ ):
631
+ """Tests that single_pass=True runs the loop only once."""
632
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
633
+ # Simulate a failure that won't be fixed to ensure the loop doesn't repeat
634
+ mock_check_file.return_value = (False, "Some unfixable error")
635
+
636
+ apply_autofix(
637
+ file_path="file.feature",
638
+ classifier="feature",
639
+ reason="any",
640
+ single_pass=True,
641
+ deterministic=False, # Disable fixes
642
+ non_deterministic=False,
643
+ classified_artefact_info=mock_classified_artefact_info,
644
+ )
645
+
646
+ output = capsys.readouterr().out
647
+ assert "Single-pass mode enabled" in output
648
+ assert "Attempt 1/1" in output
649
+ assert "Attempt 2/1" not in output
650
+ mock_check_file.assert_called_once()
651
+
652
+
653
+ @patch("ara_cli.artefact_autofix._update_rule")
654
+ @patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
655
+ def test_fix_rule_with_rule(mock_populate, mock_update_rule, mock_artefact, mock_contribution, capsys):
656
+ # Contribution has a rule
657
+ artefact_class = MagicMock()
658
+ artefact_class.deserialize.return_value = mock_artefact
659
+ mock_populate.return_value = {"info": "dummy"}
660
+
661
+ result = fix_rule(
662
+ file_path="dummy.feature",
663
+ artefact_text="text",
664
+ artefact_class=artefact_class,
665
+ classified_artefact_info={},
666
+ )
667
+
668
+ # deserialize called
669
+ artefact_class.deserialize.assert_called_once_with("text")
670
+ # _update_rule called with correct args
671
+ mock_update_rule.assert_called_once_with(
672
+ artefact=mock_artefact,
673
+ name="parent_name",
674
+ classifier="feature",
675
+ classified_file_info={"info": "dummy"},
676
+ delete_if_not_found=True,
677
+ )
678
+ # Feedback message contains rule
679
+ assert "with rule" in capsys.readouterr().out
680
+ # Result is the serialized text
681
+ assert result == "serialized-text"
682
+
683
+ @patch("ara_cli.artefact_autofix._update_rule")
684
+ @patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
685
+ def test_fix_rule_without_rule(mock_populate, mock_update_rule, mock_artefact, mock_contribution, capsys):
686
+ # Contribution rule becomes None after update
687
+ mock_contribution.rule = None
688
+ artefact_class = MagicMock()
689
+ artefact_class.deserialize.return_value = mock_artefact
690
+ mock_populate.return_value = {"info": "dummy"}
691
+
692
+ result = fix_rule(
693
+ file_path="dummy.feature",
694
+ artefact_text="text",
695
+ artefact_class=artefact_class,
696
+ classified_artefact_info={},
697
+ )
698
+
699
+ # Feedback message says "without a rule"
700
+ assert "without a rule" in capsys.readouterr().out
701
+ assert result == "serialized-text"
702
+
703
+ @patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
704
+ def test_fix_rule_contribution_none_raises(mock_populate):
705
+ # artefact.contribution is None: should assert
706
+ artefact = MagicMock()
707
+ artefact.contribution = None
708
+ artefact_class = MagicMock()
709
+ artefact_class.deserialize.return_value = artefact
710
+ mock_populate.return_value = {}
711
+
712
+ with pytest.raises(AssertionError):
713
+ fix_rule(
714
+ file_path="dummy.feature",
715
+ artefact_text="stuff",
716
+ artefact_class=artefact_class,
717
+ classified_artefact_info={},
718
+ )
719
+
720
+ def test_populate_classified_artefact_info_force_true():
721
+ """Test populate_classified_artefact_info with force=True"""
722
+ with patch('ara_cli.artefact_autofix.FileClassifier') as mock_classifier:
723
+ mock_instance = mock_classifier.return_value
724
+ mock_instance.classify_files.return_value = {"new": "data"}
725
+
726
+ result = populate_classified_artefact_info({"old": "data"}, force=True)
727
+
728
+ assert result == {"new": "data"}
729
+ mock_classifier.assert_called_once()
730
+
731
+ def test_populate_classified_artefact_info_none_input():
732
+ """Test populate_classified_artefact_info with None input"""
733
+ with patch('ara_cli.artefact_autofix.FileClassifier') as mock_classifier:
734
+ mock_instance = mock_classifier.return_value
735
+ mock_instance.classify_files.return_value = {"classified": "data"}
736
+
737
+ result = populate_classified_artefact_info(None)
738
+
739
+ assert result == {"classified": "data"}
740
+ mock_classifier.assert_called_once()
741
+
742
+ def test_parse_report_empty_content():
743
+ """Test parse_report with empty content"""
744
+ assert parse_report("") == {}
745
+
746
+ def test_parse_report_missing_reason():
747
+ """Test parse_report with missing reason in issue line"""
748
+ content = "# Artefact Check Report\n\n## feature\n- `file.feature`\n"
749
+ expected = {"feature": [("file.feature", "")]}
750
+ assert parse_report(content) == expected
751
+
752
+ def test_parse_report_multiple_classifiers():
753
+ """Test parse_report with multiple classifiers"""
754
+ content = (
755
+ "# Artefact Check Report\n\n"
756
+ "## feature\n- `file1.feature`: reason1\n\n"
757
+ "## task\n- `file2.task`: reason2\n"
758
+ )
759
+ expected = {
760
+ "feature": [("file1.feature", "reason1")],
761
+ "task": [("file2.task", "reason2")]
762
+ }
763
+ assert parse_report(content) == expected
764
+
765
+ def test_construct_prompt_non_task_artefact():
766
+ """Test construct_prompt for non-task artefact types"""
767
+ prompt = construct_prompt(ArtefactType.feature, "some reason", "file.feature", "text")
768
+ assert "For task artefacts" not in prompt
769
+ assert "some reason" in prompt
770
+ assert "file.feature" in prompt
771
+
772
+ @patch("pydantic_ai.Agent")
773
+ def test_run_agent_success(mock_agent_class):
774
+ """Test successful run_agent execution"""
775
+ mock_agent_instance = mock_agent_class.return_value
776
+ mock_result = MagicMock()
777
+ mock_result.output = "agent output"
778
+ mock_agent_instance.run_sync.return_value = mock_result
779
+
780
+ result = run_agent("test prompt", MagicMock())
781
+
782
+ assert result == "agent output"
783
+ mock_agent_class.assert_called_once()
784
+
785
+ def test_write_corrected_artefact_with_print(capsys):
786
+ """Test write_corrected_artefact prints success message"""
787
+ with patch("builtins.open", mock_open()) as m:
788
+ write_corrected_artefact("file.feature", "corrected content")
789
+
790
+ captured = capsys.readouterr()
791
+ assert "Fixed artefact at file.feature" in captured.out
792
+
793
+ # Tests for the new scenario placeholder functions
794
+ def test_extract_scenario_block():
795
+ """Test _extract_scenario_block function"""
796
+ lines = [
797
+ "Feature: Test",
798
+ "Scenario: Test scenario",
799
+ " Given something",
800
+ " When something",
801
+ "Scenario: Another scenario"
802
+ ]
803
+
804
+ scenario_lines, next_index = _extract_scenario_block(lines, 1)
805
+
806
+ assert len(scenario_lines) == 3
807
+ assert scenario_lines[0] == "Scenario: Test scenario"
808
+ assert next_index == 4
809
+
810
+ def test_is_scenario_boundary():
811
+ """Test _is_scenario_boundary function"""
812
+ assert _is_scenario_boundary("Scenario: test")
813
+ assert _is_scenario_boundary("Scenario Outline: test")
814
+ assert _is_scenario_boundary("Background:")
815
+ assert _is_scenario_boundary("Feature: test")
816
+ assert not _is_scenario_boundary("Given something")
817
+
818
+ def test_process_scenario_block_no_placeholders():
819
+ """Test _process_scenario_block with no placeholders"""
820
+ scenario_lines = [
821
+ " Scenario: Test",
822
+ " Given something",
823
+ " When something"
824
+ ]
825
+
826
+ result = _process_scenario_block(scenario_lines)
827
+
828
+ assert result == scenario_lines
829
+
830
+ def test_process_scenario_block_with_placeholders():
831
+ """Test _process_scenario_block with placeholders"""
832
+ scenario_lines = [
833
+ " Scenario: Test",
834
+ " Given something with <placeholder>",
835
+ " When something with <another>"
836
+ ]
837
+
838
+ result = _process_scenario_block(scenario_lines)
839
+
840
+ assert "Scenario Outline:" in result[0]
841
+ assert "Examples:" in result[-3]
842
+
843
+ def test_get_line_indentation():
844
+ """Test _get_line_indentation function"""
845
+ assert _get_line_indentation(" indented line") == " "
846
+ assert _get_line_indentation("no indent") == ""
847
+ assert _get_line_indentation(" two spaces") == " "
848
+
849
+ def test_extract_placeholders_from_scenario():
850
+ """Test _extract_placeholders_from_scenario function"""
851
+ step_lines = [
852
+ " Given something with <placeholder1>",
853
+ " When something with <placeholder2>",
854
+ " Then something normal"
855
+ ]
856
+
857
+ placeholders = _extract_placeholders_from_scenario(step_lines)
858
+
859
+ assert placeholders == {"placeholder1", "placeholder2"}
860
+
861
+ def test_extract_placeholders_with_docstring():
862
+ """Test _extract_placeholders_from_scenario ignoring docstrings"""
863
+ step_lines = [
864
+ " Given something with <placeholder1>",
865
+ ' """',
866
+ " Some docstring with <not_a_placeholder>",
867
+ ' """',
868
+ " When something with <placeholder2>"
869
+ ]
870
+
871
+ placeholders = _extract_placeholders_from_scenario(step_lines)
872
+
873
+ assert placeholders == {"placeholder1", "placeholder2"}
874
+
875
+ def test_update_docstring_state():
876
+ """Test _update_docstring_state function"""
877
+ assert _update_docstring_state('"""', False) == True
878
+ assert _update_docstring_state('"""', True) == False
879
+ assert _update_docstring_state('normal line', False) == False
880
+ assert _update_docstring_state('normal line', True) == True
881
+
882
+ def test_convert_to_scenario_outline():
883
+ """Test _convert_to_scenario_outline function"""
884
+ scenario_lines = [
885
+ " Scenario: Test scenario",
886
+ " Given something",
887
+ " When something"
888
+ ]
889
+ placeholders = {"placeholder1", "placeholder2"}
890
+
891
+ result = _convert_to_scenario_outline(scenario_lines, placeholders, " ")
892
+
893
+ assert "Scenario Outline: Test scenario" in result[0]
894
+ assert "Examples:" in result[-3]
895
+
896
+ def test_create_examples_table():
897
+ """Test _create_examples_table function"""
898
+ placeholders = {"param1", "param2"}
899
+
900
+ result = _create_examples_table(placeholders, " ")
901
+
902
+ assert len(result) == 3
903
+ assert "Examples:" in result[0]
904
+ assert "| param1 | param2 |" in result[1]
905
+ assert "<param1_value>" in result[2]
906
+
907
+ def test_fix_scenario_placeholder_mismatch_no_scenarios():
908
+ """Test fix_scenario_placeholder_mismatch with no scenarios"""
909
+ artefact_text = "Feature: Test\nBackground:\n Given something"
910
+
911
+ result = fix_scenario_placeholder_mismatch("file.feature", artefact_text, MagicMock())
912
+
913
+ assert result == artefact_text
914
+
915
+ def test_fix_scenario_placeholder_mismatch_with_placeholders():
916
+ """Test fix_scenario_placeholder_mismatch converting to outline"""
917
+ artefact_text = """Feature: Test
918
+ Scenario: Test scenario
919
+ Given something with <placeholder>
920
+ When something happens"""
921
+
922
+ result = fix_scenario_placeholder_mismatch("file.feature", artefact_text, MagicMock())
923
+
924
+ assert "Scenario Outline:" in result
925
+ assert "Examples:" in result
926
+ assert "<placeholder>" in result
927
+
928
+ def test_should_skip_issue_non_deterministic_false():
929
+ """Test should_skip_issue when non_deterministic is False"""
930
+ result = should_skip_issue(None, True, False, "file.txt")
931
+ assert result == True
932
+
933
+ def test_should_skip_issue_deterministic_false():
934
+ """Test should_skip_issue when deterministic is False"""
935
+ result = should_skip_issue("some_issue", False, True, "file.txt")
936
+ assert result == True
937
+
938
+ def test_should_skip_issue_no_skip():
939
+ """Test should_skip_issue when no skip is needed"""
940
+ result = should_skip_issue("some_issue", True, True, "file.txt")
941
+ assert result == False
942
+
943
+ def test_determine_attempt_count_single_pass():
944
+ """Test determine_attempt_count with single_pass=True"""
945
+ result = determine_attempt_count(True, "file.txt")
946
+ assert result == 1
947
+
948
+ def test_determine_attempt_count_multiple_pass():
949
+ """Test determine_attempt_count with single_pass=False"""
950
+ result = determine_attempt_count(False, "file.txt")
951
+ assert result == 3
952
+
953
+ def test_apply_deterministic_fix_with_issue():
954
+ """Test apply_deterministic_fix when deterministic issue exists"""
955
+ mock_fix_function = MagicMock(return_value="fixed_text")
956
+ deterministic_markers = {"test_issue": mock_fix_function}
957
+
958
+ result = apply_deterministic_fix(
959
+ True, "test_issue", "file.txt", "original", MagicMock(),
960
+ {}, deterministic_markers, "corrected"
961
+ )
962
+
963
+ assert result == "fixed_text"
964
+ mock_fix_function.assert_called_once()
965
+
966
+ def test_apply_deterministic_fix_no_issue():
967
+ """Test apply_deterministic_fix when no deterministic issue"""
968
+ result = apply_deterministic_fix(
969
+ True, None, "file.txt", "original", MagicMock(),
970
+ {}, {}, "corrected"
971
+ )
972
+
973
+ assert result == "corrected"
974
+
975
+ @patch('ara_cli.artefact_autofix.construct_prompt')
976
+ @patch('ara_cli.artefact_autofix.run_agent')
977
+ def test_apply_non_deterministic_fix_success(mock_run_agent, mock_construct_prompt):
978
+ """Test apply_non_deterministic_fix successful execution"""
979
+ mock_construct_prompt.return_value = "test prompt"
980
+ mock_artefact = MagicMock()
981
+ mock_artefact.serialize.return_value = "fixed_text"
982
+ mock_run_agent.return_value = mock_artefact
983
+
984
+ result = apply_non_deterministic_fix(
985
+ True, None, "corrected", MagicMock(), "reason",
986
+ "file.txt", "original", MagicMock()
987
+ )
988
+
989
+ assert result == "fixed_text"
990
+
991
+ def test_apply_non_deterministic_fix_with_deterministic_issue():
992
+ """Test apply_non_deterministic_fix when deterministic issue exists"""
993
+ result = apply_non_deterministic_fix(
994
+ True, "some_issue", "corrected", MagicMock(), "reason",
995
+ "file.txt", "original", MagicMock()
996
+ )
997
+
998
+ assert result == "corrected"
999
+
1000
+ @patch('ara_cli.artefact_autofix.construct_prompt')
1001
+ @patch('ara_cli.artefact_autofix.run_agent', side_effect=Exception("LLM Error"))
1002
+ def test_apply_non_deterministic_fix_exception(mock_run_agent, mock_construct_prompt, capsys):
1003
+ """Test apply_non_deterministic_fix with exception"""
1004
+ mock_construct_prompt.return_value = "test prompt"
1005
+
1006
+ result = apply_non_deterministic_fix(
1007
+ True, None, "corrected", MagicMock(), "reason",
1008
+ "file.txt", "original", MagicMock()
1009
+ )
1010
+
1011
+ assert result is None
1012
+ assert "LLM agent failed" in capsys.readouterr().out
1013
+
1014
+ @patch('ara_cli.artefact_autofix.check_file')
1015
+ @patch('ara_cli.artefact_autofix.read_artefact')
1016
+ @patch('ara_cli.artefact_autofix.write_corrected_artefact')
1017
+ @patch('ara_cli.artefact_autofix.populate_classified_artefact_info')
1018
+ @patch('ara_cli.artefact_autofix.should_skip_issue', return_value=False)
1019
+ @patch('ara_cli.artefact_autofix.apply_deterministic_fix')
1020
+ @patch('ara_cli.artefact_autofix.apply_non_deterministic_fix')
1021
+ def test_attempt_autofix_loop_max_attempts_reached(
1022
+ mock_apply_non_det, mock_apply_det, mock_should_skip, mock_populate,
1023
+ mock_write, mock_read, mock_check_file, capsys
1024
+ ):
1025
+ """Test attempt_autofix_loop when max attempts are reached"""
1026
+ mock_check_file.return_value = (False, "persistent error")
1027
+ mock_read.return_value = "original text"
1028
+ mock_apply_det.return_value = "modified text" # Ensure text is modified
1029
+ mock_apply_non_det.return_value = "modified text" # Ensure text is modified
1030
+
1031
+ result = attempt_autofix_loop(
1032
+ "file.txt", MagicMock(), MagicMock(), {}, 2, True, True, {}
1033
+ )
1034
+
1035
+ assert result == False
1036
+ assert "Failed to fix file.txt after 2 attempts" in capsys.readouterr().out
1037
+
1038
+ @patch('ara_cli.artefact_autofix.check_file')
1039
+ def test_attempt_autofix_loop_already_valid(mock_check_file, capsys):
1040
+ """Test attempt_autofix_loop when file is already valid"""
1041
+ mock_check_file.return_value = (True, "")
1042
+
1043
+ result = attempt_autofix_loop(
1044
+ "file.txt", MagicMock(), MagicMock(), {}, 3, True, True, {}
1045
+ )
1046
+
1047
+ assert result == True
1048
+ assert "is now valid" in capsys.readouterr().out
1049
+
1050
+ @patch('ara_cli.artefact_autofix.check_file')
1051
+ @patch('ara_cli.artefact_autofix.read_artefact', return_value=None)
1052
+ def test_attempt_autofix_loop_read_fails(mock_read, mock_check_file):
1053
+ """Test attempt_autofix_loop when reading artefact fails"""
1054
+ mock_check_file.return_value = (False, "some error")
1055
+
1056
+ result = attempt_autofix_loop(
1057
+ "file.txt", MagicMock(), MagicMock(), {}, 3, True, True, {}
1058
+ )
1059
+
1060
+ assert result == False